Build Status Coverage Status Codacy Rating Maven Central Bintray ScalaDoc Gitter


Primitive is a fundamental concept in phantom. Java uses the same terminology to describe its set of native types. Expanding on the same terminology, Primitives are types Cassandra can natively understand.

The Primitive trait contains all the information to serialize and de-serialize a type to and back from Cassandra, directly from a java.nio.ByteBuffer.

Default primitives and columns

This is the list of predefined available columns with their corresponding Cassandra data types. The columns are predefined for your convenience.

phantom columns Java/Scala type Cassandra type
BlobColumn java.nio.ByteBuffer blog
BigDecimalColumn scala.math.BigDecimal decimal
BigIntColumn scala.math.BigInt varint
BooleanColumn scala.Boolean boolean
DateColumn java.util.Date timestamp
DateTimeColumn org.joda.time.DateTime timestamp
DoubleColumn scala.Double double
EnumColumn scala.Enumeration text
FloatColumn scala.Float float
IntColumn scala.Int int
InetAddressColumn inet
LongColumn scala.Long long
StringColumn java.lang.String text
UUIDColumn java.util.UUID uuid
TimeUUIDColumn java.util.UUID timeuuid
ListColumn[Type] immutable.List[Type] list
SetColumn[Type] immutable.Set[Type] set
MapColumn[Type, Type] immutable.Map[Type, Type] map<type, type>
CounterColumn scala.Long counter
StaticColumn<type> <type> type static

Optional primitive columns

Optional columns allow you to set a column to a null or a None. The outcome is that instead of a T you get an Option[T] and you can match, fold, flatMap, map on a None.

The Optional part is only handled at a DSL level, it’s not translated to Cassandra in any way.

phantom columns Java/Scala type Cassandra columns
OptionalBlobColumn Option[java.nio.ByteBuffer] blog
OptionalBigDecimalColumn Option[scala.math.BigDecimal] decimal
OptionalBigIntColumn Option[scala.math.BigInt] varint
OptionalBooleanColumn Option[scala.Boolean] boolean
OptionalDateColumn Option[java.util.Date] timestamp
OptionalDateTimeColumn Option[org.joda.time.DateTime] timestamp
OptionalDoubleColumn Option[scala.Double] double
OptionalEnumColumn Option[scala.Enumeration] text
OptionalFloatColumn Option[scala.Float] float
OptionalIntColumn Option[scala.Int] int
OptionalInetAddressColumn Option[] inet
OptionalLongColumn Option[Long] long
OptionalStringColumn Option[java.lang.String] text
OptionalUUIDColumn Option[java.util.UUID] uuid
OptionalTimeUUID Option[java.util.UUID] timeuuid

It is also possible to express your entire table logic using just Col or Column if you. Primitives that do the real heavy lifting, which makes the examples below equivalent:

class Recipes extends Table[Recipes, Recipe] {

  object url extends StringColumn with PartitionKey

  object description extends OptionalStringColumn

  object ingredients extends ListColumn[String]

  object servings extends OptionalIntColumn

  object lastcheckedat extends DateTimeColumn

  object props extends MapColumn[String, String]

  object uid extends UUIDColumn

The same can be achieved by writing the below code:

class Recipes extends Table[Recipes, Recipe] {

  object url extends Col[String] with PartitionKey

  object description extends OptionalCol[String]

  object ingredients extends Col[List[String]]

  object servings extends OptionalCol[Int]

  object lastcheckedat extends Col[DateTime]

  object props extends Col[Map[String, String]]

  object uid extends Col[UUID]

Both of the above examples use the same macros under the hood which generate all Cassandra I/O required to marshall types across the wire to and from Cassandra.

The mechanism that generates primitives

Primitive is a standard typeclass. implicit resolution is used to help Cassandra understand Scala types and “pretend” they are native. This feature enables support for a wide variety of types.

Several concepts have a very direct translation into Cassandra:


A scala.Tuple can be directly mapped to a Cassandra.Tuple, which is a native Cassandra type.

Examples of such translations are visible below:

phantom columns Java/Scala type Cassandra type
Col[(String, String)] Tuple2[String, String] tuple<text, text>
Col[(String, Int)] Tuple2[String, Int] tuple<text, int>
Col[(String, Int, List[BigDecimal])] Tuple3[String, Int, List[BigDecimal]] tuple<text, int, frozen<list>>

As seen in the third example, freezing of types should happen automatically during schema auto-generation, and this is valid for any kind of type that requires freezing, including:

This also makes it possibly to derive arbitrary encodings for very complex types automatically. This feature is only available in phantom-pro using the autotables module.


case class SubRecord(
  id: UUID,
  description: String,
  tags: Set[String],
  timestamp: DateTime

object SubRecord {
  implicit val subRecordPrimitive: Primitive[SubRecord] =[SubRecord].derive

This would allow you to use SubRecord as a native type with all marshalling being at compile time with 0 runtime overhead. SubRecord is now a valid column type.

case class Record(
  id: UUID,
  name: String,
  sub: SubRecord,
  timestamp: DateTime

abstract class MyTable extends Table[MyTable, Record] {
  object id extends UUIDColumn
  object name extends StringColumn
  object sub extends Col[SubRecord]
  object timestamp extends DateTimeColumn

Deriving new primitives from existing ones

This allows you to implement new primitives based on already available ones, often it’s easier to leverage an existing implementation.

Let’s assume you are trying to derive a primitive for case class Test(value: String). Phantom allows this via Primitive.derive to produce a new implicit primitive for your custom type.

“Deriving” is simple, for a a primitive of type T that already exists, we can define a new primitive for any type X we want, provided there is a bijection from T to X, or in simple terms a way to convert a T to an X and an X back to a T` without losing any details in the process.

The implementation of derive uses context bound on Source to tell the compiler the type Source should be an already defined primitive, and the two parameters are the conversion functions from Source to Target. The output is a Primitive[Target].

def derive[
  Source : Primitive
](to: Target => Source)(from: Source => Target): Primitive[Target]

In practice, using derive for a simple scenario looks like this:

import com.outworkers.phantom.dsl._

case class Test(value: String)

object Test {
  implicit val testPrimitive: Primitive[Test] = Primitive.derive[Test, String](t => t.value)(Test.apply)

In essence, this is pretty straighforward, and now what I can do in any Cassandra table is this:

case class Wrapper(id: UUID, test: Test)

class MyTable extends CassandraTable[MyTable, Wrapper] {
  object id extends UUIDColumn with PartitionKey
  object test extends Col[Test](this)

So because of the implicit primitive, it is now safe and possible to use a Test instance everywhere in phantom, including a where clause, an insert, a set or update clause, you name it.

JDK8 Primitives

Phantom also natively supports some java.time.* JDK8 specific primitives as native types, though with a couple notable observations.

OffsetDateTime and ZonedDateTime are natively supported via the phantom-jdk8 module, and all you have to do is import com.outworkers.phantom.jdk8._. This module is only compatible with Java 8 and requires an extra dependency as a result!

val phantomVersion = ".."

libraryDependencies ++= Seq(
  "com.outworkers" %% "phantom-jdk8" % phantomVersion

Primitives for JDK8 time come in two flavours, those who can remember their timezone as part of the Cassandra marshalling and those which are automatically coerced to UTC and willingly lose timezone specificity. You would use the latter when you want to execute range queries based on these types.

When you don’t want indexing, dates are encoded as tuple<timestamp, timezone> and this is what allows to retrieve the timezone information back and have a bijective primitive.

Using the compact Table DSL with JDK 8 columns

Note unlike other columns in the framework, the JDK8 columns will require you to pass in the this argument even when you are using Table. This is a limitation of the Scala language itself, as we are not able to add class members to another class via implicit augmentation.

That’s why you should prefer to not use the now deprecated column aliases and instead rely on Col or OptionalCol.


case class Jdk8Row(
  pkey: String,
  offsetDateTime: OffsetDateTime,
  zonedDateTime: ZonedDateTime,
  localDate: LocalDate,
  localDateTime: LocalDateTime

abstract class PrimitivesJdk8 extends CassandraTable[PrimitivesJdk8, Jdk8Row] with RootConnector {

  object pkey extends StringColumn(this) with PartitionKey

  object offsetDateTime extends OffsetDateTimeColumn(this)

  object zonedDateTime extends ZonedDateTimeColumn(this)

  object localDate extends LocalDateColumn(this)

  object localDateTime extends LocalDateTimeColumn(this)
The new compact table DSL.
abstract class PrimitivesJdk8 extends Table[PrimitivesJdk8, Jdk8Row] {

  object pkey extends StringColumn with PartitionKey

  object offsetDateTime extends Col[OffsetDateTime]

  object zonedDateTime extends Col[ZonedDateTime]

  object localDate extends Col[LocalDate]

  object localDateTime extends Col[LocalDateTime]

Timezone preserving primitives

These will be by default available under import com.outworkers.phantom.jdk8._. The only exception is java.time.LocalDateTimeColumn which is indexed with both imports.

phantom columns Java/Scala type Cassandra type
OffsetDateTimeColumn java.time.OffsetDateTime tuple<bigint, text>
ZonedDateTimeColumn java.time.ZonedDateTime tuple<bigint, text>
LocalDate java.time.LocalDate localdate
LocalDateTime java.time.LocalDateTime timestamp

UTC indexed time primitives

These will be by default available under import com.outworkers.phantom.jdk8.indexed._

phantom columns Java/Scala type Cassandra type
OffsetDateTimeColumn java.time.OffsetDateTime timestamp
ZonedDateTimeColumn java.time.ZonedDateTime timestamp
LocalDate java.time.LocalDate localdate
LocalDateTime java.time.LocalDateTime timestamp