Build Status Coverage Status Codacy Rating Maven Central Bintray ScalaDoc Gitter

Select queries

This explores all the various SELECT functionality in CQL as described in the official reference here. Phantom aims to provide a 100% mapping with all the latest CQL features, but if you find anything that you need missing, please help us by reporting it through GitHub issues.

Pre-requisites and setup.

To better explain the document to follow, it is easier if we reach common ground by refering to the same Cassandra table. The below example is a simple Cassandra table with a more complex key that allows us to explore all the features of phantom and demonstrate the available select API.

We will create a AnalyticsEntries table, where we hold information about a car’s state over time for 2 properties we care about, namely the velocity and tirePressure. We leverage the TimeUUID Cassandra type to store information about timestamps, order the logs we receive in descending order(most recent record first), and prevent any collisions.

If we would just use the normal timestamp type, if we were to receive two logs for the same car at the exact same timestamp, the entries would override each other in Cassandra, because in effect they would have the same partition key and the same clustering key, so the whole primary key would be identical.


import com.datastax.driver.core.SocketOptions
import com.outworkers.phantom.dsl._

object Connector {
  val default: CassandraConnection = ContactPoint.local
    .withClusterBuilder(_.withSocketOptions(
      new SocketOptions()
        .setConnectTimeoutMillis(20000)
        .setReadTimeoutMillis(20000)
      )
    ).noHeartbeat().keySpace(
      KeySpace("phantom").ifNotExists().`with`(
        replication eqs SimpleStrategy.replication_factor(1)
      )
    )
}

case class CarMetric(
  car: UUID,
  id: UUID,
  velocity: Double,
  tirePressure: Double
)

abstract class AnalyticsEntries extends Table[AnalyticsEntries, CarMetric] {
  object car extends UUIDColumn with PartitionKey
  object id extends TimeUUIDColumn with ClusteringOrder with Descending
  object velocity extends DoubleColumn
  object tirePressure extends DoubleColumn
}

class BasicDatabase(override val connector: CassandraConnection) extends Database[BasicDatabase](connector) {
  object entries extends AnalyticsEntries with Connector
}

object db extends BasicDatabase(Connector.default)


Available query methods.

The following is the list of available query methods on a select, and it can be used to leverage standard SELECT functionality in various ways.

Method name Return type Purpose
future com.ouwotkers.phantom.ResultSet Available on all queries, returns the raw result type.
one Option[R] Select a single result as an Option[R]
fetch List[R] Select a small list of records without a paging state
fetch(modifier) List[R] Select a small list of records without a paging state
fetchRecord ListResult[R] Fetch a small result together with the ResultSet
paginateRecord ListResult[R] Fetch a paginated result together with the ResultSet and PagingState
paginateRecord(modifier) ListResult[R] Fetch a paginated result together with the ResultSet and PagingState
paginateRecord(pagingState) ListResult[R] Fetch a paginated result together with the ResultSet and PagingState

Paginating results by leveraging paging states and automated Cassandra pagination.

There are situations where you can not retrieve a whole list of results in a single go, and for that reason Cassandra offers paging states and automated pagination. Phantom makes that functionality available through a set of overloaded methods called paginateRecord.

As opposed to a normal one or fetch query, calling paginateRecord will return a ListResult, that allows you to look inside the original ResultSet, as well as the PagingState. The state can then be serialized to a string, and using that string is the key to pagination from a client.

Projections

Cassandra allows you to select only part of the columns in your tables as part of the result select. We refer to such queries as projections or partial select queries. Phantom automatically computes the type of the partial selects using tupled types.

This also enforces a limit on the number of columns you can select in a single query to a maximum of 21, because in Scala we cannot have Tuples of more than 21 type parameters. This may be possible in more recent versions of Scala, but for the sake of legacy compatibility and enforcing a more lean query API we advise that you respect this limitation.


import java.util.UUID
import scala.concurrent.Future

trait SelectExamples extends db.Connector {
  val carId = UUID.randomUUID

  // This is a select * query, selecting the entire record
  def selectAll: Future[List[CarMetric]] = {
    db.entries.select.where(_.car eqs carId).fetch()
  }

  // In this example, we are only going to select the ID column. Notice how phantom handles the type.
  def selectOnlyId: Future[List[UUID]] = {
    db.entries.select(_.car).where(_.car eqs carId).fetch()
  }

  // We can also select multiple columns, and phantom will create a tuple return type for us.
  def selectMultiple: Future[List[(UUID, UUID, Double)]] = {
    db.entries.select(_.car, _.id, _.velocity).where(_.car eqs carId).fetch()
  }
}

“IN” operator


import java.util.UUID
import scala.concurrent.Future

trait InSelectExamples extends db.Connector {
  val carId = UUID.randomUUID

  // This is a select * query, selecting the entire record
  def selectAll: Future[List[CarMetric]] = {
    db.entries.select.where(_.car in List(carId, UUID.randomUUID)).fetch()
  }
}

Due to an interesting behaviour in the Scala compiler, prepared queries that use the “IN” operator will require special binding at compile time, called ListValue.


import java.util.UUID
import scala.concurrent.Future

trait InSelectPreparedExamples extends db.Connector {

  lazy val selectInExample = db.entries.select.where(_.car in ?).prepareAsync()

  // This is a select * query, selecting the entire record
  def selectFromList(values: List[UUID]): Future[List[CarMetric]] = {
    selectInExample.flatMap(_.bind(ListValue(values)).fetch)
  }

  // We can use also use a vargargs style method call to achieve the same goal.  
  def selectFromArgs(args: UUID*): Future[List[CarMetric]] = {
    selectInExample.flatMap(_.bind(ListValue(args: _*)).fetch)
  }
}