Skip to content

Commit

Permalink
Merge pull request #116 from alexshabal/extend-cqlt-interpolator
Browse files Browse the repository at this point in the history
Added Columns, Values, KeyEqualsTo and Assignment types for cqlt interpolator
  • Loading branch information
Alexey-Yuferov authored Dec 2, 2024
2 parents 57cbefa + b03e9ed commit 415c3e5
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 20 deletions.
2 changes: 1 addition & 1 deletion .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version = "3.7.14"
version = "2.7.5"
maxColumn = 120
align = most
continuationIndent.defnSite = 2
Expand Down
49 changes: 33 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,30 +90,47 @@ import scala.jdk.DurationConverters._
import com.datastax.oss.driver.api.core.ConsistencyLevel
import com.ringcentral.cassandra4io.CassandraSession
import com.ringcentral.cassandra4io.cql._

case class Model(id: Int, data: String)


case class Model(pk: Long, ck: String, data: String, metaData: String)
case class Key(pk: Long, ck: String)
case class Data(data: String, metaData: String)

trait Dao[F[_]] {
def put(value: Model): F[Unit]
def get(id: Int): F[Option[Model]]
def update(key: Key, data: Data): F[Unit]
def get(key: Key): F[Option[Model]]
}

object Dao {

private val tableName = "table"
private val insertQuery = cqlt"insert into ${Const(tableName)} (id, data) values (${Put[Int]}, ${Put[String]})"
.config(_.setTimeout(1.second.toJava))
private val selectQuery = cqlt"select id, data from ${Const(tableName)} where id = ${Put[Int]}".as[Model]

def apply[F[_]: Async](session: CassandraSession[F]) = for {
private val tableName = "table"
private val insertQuery =
cqlt"insert into ${Const(tableName)} (pk, ck, data, meta_data) values (${Put[Long]}, ${Put[String]}, ${Put[String]}, ${Put[String]})"
.config(_.setTimeout(1.second.toJava))
private val insertQueryAlternative =
cqlt"insert into ${Const(tableName)} (${Columns[Model]}) values (${Values[Model]})"
private val updateQuery = cqlt"update ${Const(tableName)} set ${Assignment[Data]} where ${EqualsTo[Key]}"
private val selectQuery = cqlt"select ${Columns[Model]} from ${Const(tableName)} where ${EqualsTo[Key]}".as[Model]

def apply[F[_] : Async](session: CassandraSession[F]) = for {
insert <- insertQuery.prepare(session)
select <- selectQuery.prepare(session)
update <- updateQuery.prepare(session)
insertAlternative <- insertQueryAlternative.prepare(session)
select <- selectQuery.prepare(session)
} yield new Dao[F] {
override def put(value: Model) = insert(value.id, value.data).execute.void
override def get(id: Int) = select(id).config(_.setExecutionProfileName("default")).select.head.compile.last
}
}
override def put(value: Model) = insert(
value.pk,
value.ck,
value.data,
value.metaData
).execute.void // insertAlternative(value).execute.void
override def update(key: Key, data: Data): F[Unit] = updateQuery(data, key).execute.void
override def get(key: Key) = select(key).config(_.setExecutionProfileName("default")).select.head.compile.last
}
}
```
As you can see `${Columns[Model]}` expands to `pk, ck, data, meta_data`, `${Values[Model]}` to `?, ?, ?, ?`, `${Assignment[Data]}` to `data = ?, meta_data = ?` and `${EqualsTo[Key]}` expands to `pk = ? and ck = ?`.
Latter three types adjust query type as well for being able to bind corresponding values

### Handling optional fields (`null`)

Expand Down
9 changes: 9 additions & 0 deletions src/it/resources/migration/1__test_tables.cql
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,12 @@ CREATE TABLE heavily_nested_prim_table(
data example_nested_primitive_type,
PRIMARY KEY (id)
);

create table test_data_interpolated(
key bigint,
projection_key text,
projection_data text,
offset bigint,
timestamp bigint,
PRIMARY KEY (key, projection_key)
);
49 changes: 49 additions & 0 deletions src/it/scala/com/ringcentral/cassandra4io/cql/CqlSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,55 @@ trait CqlSuite {
} yield expect(results == Seq(Data(1, "one"), Data(2, "two"), Data(3, "three")))
}

test(
"interpolated inserts and selects should work with derived KeyEquals, Columns and Values"
) { session =>
case class Table(key: Long, projectionKey: String, projectionData: String, offset: Long, timestamp: Long)
case class Key(key: Long, projectionKey: String)

val insert = cqlt"INSERT INTO ${Const("test_data_interpolated")}(${Columns[Table]}) VALUES (${Values[Table]})"
val select =
cqlt"SELECT ${Columns[Table]} FROM ${Const("test_data_interpolated")} WHERE ${EqualsTo[Key]}"
.as[Table]

val data1 = Table(1, "projection-1", "data-1", 1, 1732547921580L)
val data2 = Table(1, "projection-2", "data-1", 2, 1732547921586L)
val key = Key(1, "projection-1")

for {
preparedInsert <- insert.prepare(session)
preparedSelect <- select.prepare(session)
_ <- preparedInsert(data1).execute
_ <- preparedInsert(data2).execute
result <- preparedSelect(key).select.compile.toList
} yield expect(result == List(data1))
}

test(
"interpolated updates and selects should work with derived KeyEquals and Assignment"
) { session =>
case class Data(projectionData: String, offset: Long, timestamp: Long)
case class Key(key: Long, projectionKey: String)

val update = cqlt"UPDATE ${Const("test_data_interpolated")} SET ${Assignment[Data]} WHERE ${EqualsTo[Key]}"
val select =
cqlt"SELECT ${Columns[Data]} FROM ${Const("test_data_interpolated")} WHERE ${EqualsTo[Key]}"
.as[Data]

val data1 = Data("data-1", 1, 1732547921580L)
val data2 = Data("data-1", 2, 1732547921586L)
val key1 = Key(2, "projection-1")
val key2 = Key(2, "projection-2")

for {
preparedUpdate <- update.prepare(session)
preparedSelect <- select.prepare(session)
_ <- preparedUpdate(data1, key1).execute
_ <- preparedUpdate(data2, key2).execute
result <- preparedSelect(key1).select.compile.toList
} yield expect(result == List(data1))
}

test(
"interpolated inserts and selects should produce UDTs and return data case classes when nested case classes are used"
) { session =>
Expand Down
121 changes: 118 additions & 3 deletions src/main/scala/com/ringcentral/cassandra4io/cql/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.datastax.oss.driver.api.core.cql._
import com.datastax.oss.driver.api.core.data.UdtValue
import fs2.Stream
import shapeless._
import shapeless.labelled.FieldType
import shapeless.ops.hlist.Prepend

import java.nio.ByteBuffer
Expand Down Expand Up @@ -125,9 +126,17 @@ package object cql {
QueryTemplate[V, Row](
ctx.parts
.foldLeft[(HList, StringBuilder)]((params, new StringBuilder())) {
case ((Const(const) :: tail, builder), part) => (tail, builder.appendAll(part).appendAll(const))
case ((_ :: tail, builder), part) => (tail, builder.appendAll(part).appendAll("?"))
case ((HNil, builder), part) => (HNil, builder.appendAll(part))
case ((Const(const) :: tail, builder), part) => (tail, builder.appendAll(part).appendAll(const))
case (((restriction: EqualsTo[_]) :: tail, builder), part) =>
(tail, builder.appendAll(part).appendAll(restriction.keys.map(key => s"${key} = ?").mkString(" AND ")))
case (((assignment: Assignment[_]) :: tail, builder), part) =>
(tail, builder.appendAll(part).appendAll(assignment.keys.map(key => s"${key} = ?").mkString(", ")))
case (((columns: Columns[_]) :: tail, builder), part) =>
(tail, builder.appendAll(part).appendAll(columns.keys.mkString(", ")))
case (((values: Values[_]) :: tail, builder), part) =>
(tail, builder.appendAll(part).appendAll(List.fill(values.size)("?").mkString(", ")))
case ((_ :: tail, builder), part) => (tail, builder.appendAll(part).appendAll("?"))
case ((HNil, builder), part) => (HNil, builder.appendAll(part))
}
._2
.toString(),
Expand Down Expand Up @@ -166,6 +175,25 @@ package object cql {
override type Repr = RT
override def binder: Binder[RT] = f.binder
}

implicit def hConsBindableColumnsBuilder[T, PT <: HList, RT <: HList](implicit
f: BindableBuilder.Aux[PT, RT]
): BindableBuilder.Aux[Columns[T] :: PT, RT] =
new BindableBuilder[Columns[T] :: PT] {
override type Repr = RT
override def binder: Binder[RT] = f.binder
}

implicit def hConsBindableValuesBuilder[V[_] <: Values[_], T: ColumnsValues, PT <: HList, RT <: HList](implicit
f: BindableBuilder.Aux[PT, RT]
): BindableBuilder.Aux[V[T] :: PT, T :: RT] = new BindableBuilder[V[T] :: PT] {
override type Repr = T :: RT
override def binder: Binder[T :: RT] = {
implicit val hBinder: Binder[T] = Values[T].binder
implicit val tBinder: Binder[RT] = f.binder
Binder[T :: RT]
}
}
}
}

Expand Down Expand Up @@ -261,6 +289,93 @@ package object cql {
}

case class Const(fragment: String)
trait Columns[T] {
def keys: List[String]
}
object Columns {
def apply[T: ColumnsValues]: Columns[T] = new Columns[T] {
override def keys: List[String] = ColumnsValues[T].keys
}
}
trait Values[T] {
def size: Int
def binder: Binder[T]
}
object Values {
def apply[T: ColumnsValues]: Values[T] = new Values[T] {
override def size: Int = ColumnsValues[T].size
override def binder: Binder[T] = ColumnsValues[T].binder
}
}
trait EqualsTo[T] extends Columns[T] with Values[T]
object EqualsTo {
def apply[T: ColumnsValues]: EqualsTo[T] = new EqualsTo[T] {
override def keys: List[String] = ColumnsValues[T].keys
override def size: Int = ColumnsValues[T].size
override def binder: Binder[T] = ColumnsValues[T].binder
}
}

trait Assignment[T] extends Columns[T] with Values[T]
object Assignment {
def apply[T: ColumnsValues]: Assignment[T] = new Assignment[T] {
override def keys: List[String] = ColumnsValues[T].keys
override def size: Int = ColumnsValues[T].size
override def binder: Binder[T] = ColumnsValues[T].binder
}
}

private trait ColumnsValues[T] extends Columns[T] with Values[T]
private object ColumnsValues {
def apply[T](implicit ev: ColumnsValues[T]): ColumnsValues[T] = ev

implicit val hNilColumnsValues: ColumnsValues[HNil] = new ColumnsValues[HNil] {
override def keys: List[String] = List.empty
override def size: Int = 0
override def binder: Binder[HNil] = Binder.hNilBinder
}

private def camel2snake(text: String) =
text.tail.foldLeft(text.headOption.fold("")(_.toLower.toString)) {
case (acc, c) if c.isUpper => acc + "_" + c.toLower
case (acc, c) => acc + c
}

implicit def hListColumnsValues[K, V, T <: HList](implicit
witness: Witness.Aux[K],
tColumnsValues: ColumnsValues[T],
vBinder: Binder[V]
): ColumnsValues[FieldType[K, V] :: T] =
new ColumnsValues[FieldType[K, V] :: T] {
override def keys: List[String] = {
val key = witness.value match {
case Symbol(key) => camel2snake(key)
case _ => witness.value.toString
}
key :: tColumnsValues.keys
}
override def size: Int = tColumnsValues.size + 1
override def binder: Binder[FieldType[K, V] :: T] = {
implicit val hBinder: Binder[FieldType[K, V]] = new Binder[FieldType[K, V]] {
override def bind(statement: BoundStatement, index: Int, value: FieldType[K, V]): (BoundStatement, Int) =
vBinder.bind(statement, index, value)
}
implicit val tBinder: Binder[T] = tColumnsValues.binder
Binder[FieldType[K, V] :: T]
}
}
implicit def genColumnValues[T, TRepr](implicit
gen: LabelledGeneric.Aux[T, TRepr],
columnsValues: ColumnsValues[TRepr]
): ColumnsValues[T] = new ColumnsValues[T] {
override def keys: List[String] = columnsValues.keys
override def size: Int = columnsValues.size
override def binder: Binder[T] = new Binder[T] {
override def bind(statement: BoundStatement, index: Int, value: T): (BoundStatement, Int) =
columnsValues.binder.bind(statement, index, gen.to(value))
}
}
}

object Binder extends BinderLowerPriority with BinderLowestPriority {

Expand Down

0 comments on commit 415c3e5

Please sign in to comment.