Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Columns, Values, KeyEqualsTo and Assignment types for cqlt interpolator #116

Merged
merged 11 commits into from
Dec 2, 2024
Merged
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