Skip to content
/ cql4s Public

Cassandra typesafe DSL for pure functional Scala supporting cats effect, fs2, ZIO

License

Notifications You must be signed in to change notification settings

epifab/cql4s

Repository files navigation

CQL4s

Build and test

CQL4s is a typesafe CQL DSL and Cassandra client for Scala 3.

Why CQL4s

CQL4s brings Cassandra to the wonderful world of functional Scala, by supporting out of the box cats effect / fs2 and ZIO 2.

CQL4s is a pure functional layer built on top of the Datastax Java driver.
The main difference with similar libraries lies on a strongly typed, "as close as possible to cql" DSL, that will help to prevent common mistakes at compile time, such as referring to a non-existing table, type or column, comparing fields of unrelated types, decoding the results of a query into an incompatible data structure and issues related to placeholders encoding.

CQL4s was strongly inspired by Tydal.

Usage example

The model

Simple, dependency-free model for the data stored in Cassandra.

import java.time.Instant
import java.util.{Currency, UUID}

case class User(name: String, email: Option[String], phone: (Short, String))

case class Metadata(createdAt: Instant, updatedAt: Option[Instant], author: User)

case class Event(
  id: UUID,
  startTime: Instant,
  artists: List[String],
  venue: String,
  tickets: Map[Currency, BigDecimal],
  tags: Set[String],
  metadata: Metadata
)

The schema

The schema includes all tables, user defined types and custom types modelled on top of existing ones.

import cql4s.dsl.*

import java.util.Currency

// ------------
// Custom types
// ------------

trait currencyType

object currencyType:
  given DataTypeCodec[currencyType, String, Currency] = 
    DataType.textCodec.map(_.getCurrencyCode, Currency.getInstance)


// ------------------
// User defined types
// ------------------

class userType extends udt[
  User,     // model case class
  "music",  // keyspace
  "user",   // udt name
  (
    "name"  :=: text,
    "email" :=: nullable[text],
    "phone" :=: (smallint, text)  // tuple
  )
]

class metadataType extends udt[
  Metadata,   // model case class
  "music",    // keyspace
  "metadata", // udt name
  (
    "createdAt" :=: timestamp,
    "updatedAt" :=: nullable[timestamp],
    "author"    :=: userType   // nested udt
  )
]

// ------
// Tables
// ------

object events extends Table[
  "music",  // keyspace
  "events", // table name
  (
    "id"         :=: uuid,
    "start_time" :=: timestamp,
    "artists"    :=: list[text],
    "venue"      :=: text,
    "tickets"    :=: map[currencyType, decimal],
    "tags"       :=: set[text],
    "metadata"   :=: metadataType
  )
]

Queries and commands

Typically, you might want to extract away all your queries and commands.

import cql4s.CassandraRuntimeAlgebra
import cql4s.dsl.*

import java.util.{Currency, UUID}
import scala.util.chaining.*

class EventsRepo[F[_], S[_]](using CassandraRuntimeAlgebra[F, S]):
  val add: Event => F[Unit] =
    Insert
      .into(events)
      .compile
      .pcontramap[Event]
      .execute

  val updateTickets: (Map[Currency, BigDecimal], Metadata, UUID) => F[Unit] =
    Update(events)
      .set(e => (e("tickets"), e("metadata")))
      .where(_("id") === :?)
      .compile
      .execute
      .pipe(Function.untupled)

  val findByIds: List[UUID] => S[Event] =
    Select
      .from(events)
      .take(_.*)
      .where(_("id") in :?)
      .compile
      .pmap[Event]
      .stream

The app (typelevel example)

Here's what the application might look like in cats effect/fs2 land:

import cats.effect.{ExitCode, IO, IOApp}
import cql4s.{CassandraCatsRuntime, CassandraConfig}

import java.time.Instant
import java.util.{Currency, UUID}

object Program extends IOApp:
  val cassandraConfig = CassandraConfig(
    "0.0.0.0",
    9042,
    credentials = None,
    keyspace = None,
    datacenter = "testdc"
  )
  
  def run(args: List[String]): IO[ExitCode] =
    CassandraCatsRuntime[IO](cassandraConfig)
      .map(cassandra => new EventsRepo(using cassandra))
      .use { repo =>
        repo
          .findByIds(args.map(UUID.fromString))
          // Update existing event tickets
          .evalTap(e => repo.updateTickets(
            Map(Currency.getInstance("GBP") -> 49.99),
            e.metadata.copy(updatedAt = Some(Instant.now)),
            e.id
          ))
          // Create a new event with same artists on a different day
          .evalTap(e => repo.add(e.copy(
            id = UUID.randomUuid(),
            startTime = Instant.parse("2022-03-08T20:30:00Z"),
            metadata = e.metadata.copy(createdAt = Instant.now, updatedAt = None)
          )))
          .compile
          .drain
          .as(ExitCode.Success)
      }

The app (ZIO example)

Here's what the application might look like in ZIO land:

import cql4s.{CassandraConfig, CassandraZIORuntime, CassandraZLayer}
import zio.{ZIO, ZIOAppArgs, ZIOAppDefault}

import java.time.Instant
import java.util.{Currency, UUID}

object Program extends ZIOAppDefault:
  val cassandraLayer = CassandraZLayer(CassandraConfig(
    "0.0.0.0",
    9042,
    credentials = None,
    keyspace = None,
    datacenter = "testdc"
  ))

  val repo = new EventsRepo(using CassandraZIORuntime)

  val app = ZIO.service[ZIOAppArgs].flatMap(args =>
    repo
      .findByIds(args.getArgs.toList.map(UUID.fromString))
      // Update existing event price
      .tap(e => repo.updateTickets(
        Map(Currency.getInstance("GBP") -> 49.99),
        e.metadata.copy(updatedAt = Some(Instant.now)),
        e.id
      ))
      // Create a new event with same artists on a different day
      .tap(e => repo.add(
        e.copy(
          id = UUID.randomUUID(),
          startTime = Instant.parse("2022-03-08T20:30:00Z"),
          metadata = e.metadata.copy(createdAt = Instant.now, updatedAt = None)
        )
      ))
      .runDrain
  )
  
  val run = app.provideSomeLayer(cassandraLayer)

Support

Find out all supported feature here.

Getting started

Installation (sbt)

Step 1. Add the JitPack repository to your build file

resolvers += "jitpack" at "https://jitpack.io"

Step 2. Add the dependencies

For the typelevel stack:

libraryDependencies ++= Seq(
    "com.github.epifab.cql4s" %% "cql4s-core",
    "com.github.epifab.cql4s" %% "cql4s-cats"
).map(_ % Version)

For ZIO:

libraryDependencies ++= Seq(
    "com.github.epifab.cql4s" %% "cql4s-core",
    "com.github.epifab.cql4s" %% "cql4s-zio"
).map(_ % Version)

Optional: JSON support using circe

libraryDependencies += "com.github.epifab.cql4s" %% "cql4s-circe" % Version

Testing

$ docker-compose up -d
$ sbt test

About

Cassandra typesafe DSL for pure functional Scala supporting cats effect, fs2, ZIO

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages