From 63a518f5ecd6907ad78cea46dbe5e5f0b4454d9e Mon Sep 17 00:00:00 2001 From: Aleksandr Shabalin Date: Mon, 25 Nov 2024 18:51:35 +0300 Subject: [PATCH 01/11] Added doc for Columns, Values and PrimaryKey types for cqlt interpolator --- src/it/resources/migration/1__test_tables.cql | 9 ++ .../cassandra4io/cql/CqlSuite.scala | 26 +++- .../cassandra4io/cql/package.scala | 115 +++++++++++++++++- 3 files changed, 146 insertions(+), 4 deletions(-) diff --git a/src/it/resources/migration/1__test_tables.cql b/src/it/resources/migration/1__test_tables.cql index 26c5751..92b015f 100644 --- a/src/it/resources/migration/1__test_tables.cql +++ b/src/it/resources/migration/1__test_tables.cql @@ -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, + projectionKey text, + projectionData text, + offset bigint, + timestamp bigint, + PRIMARY KEY (key, projectionKey) +); diff --git a/src/it/scala/com/ringcentral/cassandra4io/cql/CqlSuite.scala b/src/it/scala/com/ringcentral/cassandra4io/cql/CqlSuite.scala index a12a19e..af115b3 100644 --- a/src/it/scala/com/ringcentral/cassandra4io/cql/CqlSuite.scala +++ b/src/it/scala/com/ringcentral/cassandra4io/cql/CqlSuite.scala @@ -7,7 +7,7 @@ import com.ringcentral.cassandra4io.CassandraTestsSharedInstances import fs2.Stream import weaver._ -import java.time.{ Duration, LocalDate, LocalTime } +import java.time.{Duration, LocalDate, LocalTime} import java.util.UUID import java.util.concurrent.atomic.AtomicInteger @@ -194,6 +194,30 @@ trait CqlSuite { } yield expect(results == Seq(Data(1, "one"), Data(2, "two"), Data(3, "three"))) } + test( + "interpolated inserts and selects should work with derived 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 ${PrimaryKey[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 inserts and selects should produce UDTs and return data case classes when nested case classes are used" ) { session => diff --git a/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala b/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala index 4b5f72e..8f5f3f0 100644 --- a/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala +++ b/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala @@ -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 @@ -125,9 +126,15 @@ 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 (((ands: PrimaryKey[_]) :: tail, builder), part) => + (tail, builder.appendAll(part).appendAll(ands.keys.map(key => s"${key} = ?").mkString(" AND "))) + 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(), @@ -166,6 +173,36 @@ 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[T: ColumnsValues, PT <: HList, RT <: HList](implicit + f: BindableBuilder.Aux[PT, RT] + ): BindableBuilder.Aux[Values[T] :: PT, T :: RT] = new BindableBuilder[Values[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] + } + } + + implicit def hConsBindableAndsBuilder[T: ColumnsValues, PT <: HList, RT <: HList](implicit + f: BindableBuilder.Aux[PT, RT] + ): BindableBuilder.Aux[PrimaryKey[T] :: PT, T :: RT] = new BindableBuilder[PrimaryKey[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] + } + } } } @@ -261,6 +298,78 @@ 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 PrimaryKey[T] extends Columns[T] with Values[T] + object PrimaryKey { + def apply[T: ColumnsValues]: PrimaryKey[T] = new PrimaryKey[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 ColumnsValues[T] extends Columns[T] with Values[T] + 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 + } + + 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) => 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 { From 7a53865e975a4985f07380cb1352d3921226f306 Mon Sep 17 00:00:00 2001 From: Aleksandr Shabalin Date: Tue, 26 Nov 2024 10:30:46 +0300 Subject: [PATCH 02/11] Changes camel to snake --- src/it/resources/migration/1__test_tables.cql | 6 +++--- .../scala/com/ringcentral/cassandra4io/cql/package.scala | 8 +++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/it/resources/migration/1__test_tables.cql b/src/it/resources/migration/1__test_tables.cql index 92b015f..1c14b28 100644 --- a/src/it/resources/migration/1__test_tables.cql +++ b/src/it/resources/migration/1__test_tables.cql @@ -77,9 +77,9 @@ CREATE TABLE heavily_nested_prim_table( create table test_data_interpolated( key bigint, - projectionKey text, - projectionData text, + projection_key text, + projection_data text, offset bigint, timestamp bigint, - PRIMARY KEY (key, projectionKey) + PRIMARY KEY (key, projection_key) ); diff --git a/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala b/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala index 8f5f3f0..b3d9dff 100644 --- a/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala +++ b/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala @@ -335,6 +335,12 @@ package object cql { 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], @@ -343,7 +349,7 @@ package object cql { new ColumnsValues[FieldType[K, V] :: T] { override def keys: List[String] = { val key = witness.value match { - case Symbol(key) => key + case Symbol(key) => camel2snake(key) case _ => witness.value.toString } key :: tColumnsValues.keys From bbea64ab2fb15f3bce2be695a5202eeccc0fae58 Mon Sep 17 00:00:00 2001 From: Aleksandr Shabalin Date: Tue, 26 Nov 2024 13:41:26 +0300 Subject: [PATCH 03/11] added Assignment and renamed PrimaryKey to KeyEquals --- .../cassandra4io/cql/CqlSuite.scala | 29 +++++++++- .../cassandra4io/cql/package.scala | 54 +++++++++++++------ 2 files changed, 65 insertions(+), 18 deletions(-) diff --git a/src/it/scala/com/ringcentral/cassandra4io/cql/CqlSuite.scala b/src/it/scala/com/ringcentral/cassandra4io/cql/CqlSuite.scala index af115b3..df258ca 100644 --- a/src/it/scala/com/ringcentral/cassandra4io/cql/CqlSuite.scala +++ b/src/it/scala/com/ringcentral/cassandra4io/cql/CqlSuite.scala @@ -195,14 +195,14 @@ trait CqlSuite { } test( - "interpolated inserts and selects should work with derived Columns and Values" + "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 ${PrimaryKey[Key]}" + cqlt"SELECT ${Columns[Table]} FROM ${Const("test_data_interpolated")} WHERE ${KeyEquals[Key]}" .as[Table] val data1 = Table(1, "projection-1", "data-1", 1, 1732547921580L) @@ -218,6 +218,31 @@ trait CqlSuite { } 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 ${KeyEquals[Key]}" + val select = + cqlt"SELECT ${Columns[Data]} FROM ${Const("test_data_interpolated")} WHERE ${KeyEquals[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 => diff --git a/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala b/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala index b3d9dff..ba436df 100644 --- a/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala +++ b/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala @@ -126,15 +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 (((ands: PrimaryKey[_]) :: tail, builder), part) => - (tail, builder.appendAll(part).appendAll(ands.keys.map(key => s"${key} = ?").mkString(" AND "))) - case (((columns: Columns[_]) :: tail, builder), part) => + case ((Const(const) :: tail, builder), part) => (tail, builder.appendAll(part).appendAll(const)) + case (((restriction: KeyEquals[_]) :: 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) => + 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)) + case ((_ :: tail, builder), part) => (tail, builder.appendAll(part).appendAll("?")) + case ((HNil, builder), part) => (HNil, builder.appendAll(part)) } ._2 .toString(), @@ -193,9 +195,20 @@ package object cql { } } - implicit def hConsBindableAndsBuilder[T: ColumnsValues, PT <: HList, RT <: HList](implicit + implicit def hConsBindableRestrictionsBuilder[T: ColumnsValues, PT <: HList, RT <: HList](implicit f: BindableBuilder.Aux[PT, RT] - ): BindableBuilder.Aux[PrimaryKey[T] :: PT, T :: RT] = new BindableBuilder[PrimaryKey[T] :: PT] { + ): BindableBuilder.Aux[KeyEquals[T] :: PT, T :: RT] = new BindableBuilder[KeyEquals[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] + } + } + + implicit def hConsBindableAssignmentsBuilder[T: ColumnsValues, PT <: HList, RT <: HList](implicit + f: BindableBuilder.Aux[PT, RT] + ): BindableBuilder.Aux[Assignment[T] :: PT, T :: RT] = new BindableBuilder[Assignment[T] :: PT] { override type Repr = T :: RT override def binder: Binder[T :: RT] = { implicit val hBinder: Binder[T] = Values[T].binder @@ -298,27 +311,36 @@ package object cql { } case class Const(fragment: String) - trait Columns[T] { + trait Columns[T] { def keys: List[String] } - object Columns { + object Columns { def apply[T: ColumnsValues]: Columns[T] = new Columns[T] { override def keys: List[String] = ColumnsValues[T].keys } } - trait Values[T] { + trait Values[T] { def size: Int def binder: Binder[T] } - object Values { + 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 PrimaryKey[T] extends Columns[T] with Values[T] - object PrimaryKey { - def apply[T: ColumnsValues]: PrimaryKey[T] = new PrimaryKey[T] { + trait KeyEquals[T] extends Columns[T] with Values[T] + object KeyEquals { + def apply[T: ColumnsValues]: KeyEquals[T] = new KeyEquals[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 From cdd3f8d889069d7be88da9142efb2e4be97bd300 Mon Sep 17 00:00:00 2001 From: Aleksandr Shabalin Date: Tue, 26 Nov 2024 13:44:43 +0300 Subject: [PATCH 04/11] renamed PrimaryKey to KeyEqualsTo --- .../com/ringcentral/cassandra4io/cql/CqlSuite.scala | 6 +++--- .../com/ringcentral/cassandra4io/cql/package.scala | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/it/scala/com/ringcentral/cassandra4io/cql/CqlSuite.scala b/src/it/scala/com/ringcentral/cassandra4io/cql/CqlSuite.scala index df258ca..e2a0124 100644 --- a/src/it/scala/com/ringcentral/cassandra4io/cql/CqlSuite.scala +++ b/src/it/scala/com/ringcentral/cassandra4io/cql/CqlSuite.scala @@ -202,7 +202,7 @@ trait CqlSuite { 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 ${KeyEquals[Key]}" + cqlt"SELECT ${Columns[Table]} FROM ${Const("test_data_interpolated")} WHERE ${KeyEqualsTo[Key]}" .as[Table] val data1 = Table(1, "projection-1", "data-1", 1, 1732547921580L) @@ -224,9 +224,9 @@ trait CqlSuite { 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 ${KeyEquals[Key]}" + val update = cqlt"UPDATE ${Const("test_data_interpolated")} SET ${Assignment[Data]} WHERE ${KeyEqualsTo[Key]}" val select = - cqlt"SELECT ${Columns[Data]} FROM ${Const("test_data_interpolated")} WHERE ${KeyEquals[Key]}" + cqlt"SELECT ${Columns[Data]} FROM ${Const("test_data_interpolated")} WHERE ${KeyEqualsTo[Key]}" .as[Data] val data1 = Data("data-1", 1, 1732547921580L) diff --git a/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala b/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala index ba436df..1f079c2 100644 --- a/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala +++ b/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala @@ -127,7 +127,7 @@ package object cql { ctx.parts .foldLeft[(HList, StringBuilder)]((params, new StringBuilder())) { case ((Const(const) :: tail, builder), part) => (tail, builder.appendAll(part).appendAll(const)) - case (((restriction: KeyEquals[_]) :: tail, builder), part) => + case (((restriction: KeyEqualsTo[_]) :: 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(", "))) @@ -197,7 +197,7 @@ package object cql { implicit def hConsBindableRestrictionsBuilder[T: ColumnsValues, PT <: HList, RT <: HList](implicit f: BindableBuilder.Aux[PT, RT] - ): BindableBuilder.Aux[KeyEquals[T] :: PT, T :: RT] = new BindableBuilder[KeyEquals[T] :: PT] { + ): BindableBuilder.Aux[KeyEqualsTo[T] :: PT, T :: RT] = new BindableBuilder[KeyEqualsTo[T] :: PT] { override type Repr = T :: RT override def binder: Binder[T :: RT] = { implicit val hBinder: Binder[T] = Values[T].binder @@ -329,9 +329,9 @@ package object cql { override def binder: Binder[T] = ColumnsValues[T].binder } } - trait KeyEquals[T] extends Columns[T] with Values[T] - object KeyEquals { - def apply[T: ColumnsValues]: KeyEquals[T] = new KeyEquals[T] { + trait KeyEqualsTo[T] extends Columns[T] with Values[T] + object KeyEqualsTo { + def apply[T: ColumnsValues]: KeyEqualsTo[T] = new KeyEqualsTo[T] { override def keys: List[String] = ColumnsValues[T].keys override def size: Int = ColumnsValues[T].size override def binder: Binder[T] = ColumnsValues[T].binder From 0bc24dc04946cf7cd3368115be7124873712dc1f Mon Sep 17 00:00:00 2001 From: Aleksandr Shabalin Date: Tue, 26 Nov 2024 14:13:54 +0300 Subject: [PATCH 05/11] add lazy to implicit derivations --- .../cassandra4io/cql/package.scala | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala b/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala index 1f079c2..faf738d 100644 --- a/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala +++ b/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala @@ -126,17 +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 ((Const(const) :: tail, builder), part) => (tail, builder.appendAll(part).appendAll(const)) case (((restriction: KeyEqualsTo[_]) :: tail, builder), part) => (tail, builder.appendAll(part).appendAll(restriction.keys.map(key => s"${key} = ?").mkString(" AND "))) - case (((assignment: Assignment[_]) :: tail, builder), part) => + case (((assignment: Assignment[_]) :: tail, builder), part) => (tail, builder.appendAll(part).appendAll(assignment.keys.map(key => s"${key} = ?").mkString(", "))) - case (((columns: Columns[_]) :: tail, builder), part) => + case (((columns: Columns[_]) :: tail, builder), part) => (tail, builder.appendAll(part).appendAll(columns.keys.mkString(", "))) - case (((values: Values[_]) :: tail, builder), part) => + 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)) + case ((_ :: tail, builder), part) => (tail, builder.appendAll(part).appendAll("?")) + case ((HNil, builder), part) => (HNil, builder.appendAll(part)) } ._2 .toString(), @@ -311,19 +311,19 @@ package object cql { } case class Const(fragment: String) - trait Columns[T] { + trait Columns[T] { def keys: List[String] } - object Columns { + object Columns { def apply[T: ColumnsValues]: Columns[T] = new Columns[T] { override def keys: List[String] = ColumnsValues[T].keys } } - trait Values[T] { + trait Values[T] { def size: Int def binder: Binder[T] } - object Values { + 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 From 0db82be8ec7d3a8a00593e3ccc07fe81f3c60f9a Mon Sep 17 00:00:00 2001 From: Aleksandr Shabalin Date: Tue, 26 Nov 2024 17:02:51 +0300 Subject: [PATCH 06/11] optimized hConsBindableValuesBuilder --- .../cassandra4io/cql/package.scala | 26 ++----------------- 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala b/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala index faf738d..89b71bb 100644 --- a/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala +++ b/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala @@ -184,31 +184,9 @@ package object cql { override def binder: Binder[RT] = f.binder } - implicit def hConsBindableValuesBuilder[T: ColumnsValues, PT <: HList, RT <: HList](implicit + implicit def hConsBindableValuesBuilder[V[_] <: Values[_], T: ColumnsValues, PT <: HList, RT <: HList](implicit f: BindableBuilder.Aux[PT, RT] - ): BindableBuilder.Aux[Values[T] :: PT, T :: RT] = new BindableBuilder[Values[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] - } - } - - implicit def hConsBindableRestrictionsBuilder[T: ColumnsValues, PT <: HList, RT <: HList](implicit - f: BindableBuilder.Aux[PT, RT] - ): BindableBuilder.Aux[KeyEqualsTo[T] :: PT, T :: RT] = new BindableBuilder[KeyEqualsTo[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] - } - } - - implicit def hConsBindableAssignmentsBuilder[T: ColumnsValues, PT <: HList, RT <: HList](implicit - f: BindableBuilder.Aux[PT, RT] - ): BindableBuilder.Aux[Assignment[T] :: PT, T :: RT] = new BindableBuilder[Assignment[T] :: PT] { + ): 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 From 0d03992433814eed449fcdd21f5607fbabbeca0e Mon Sep 17 00:00:00 2001 From: Aleksandr Shabalin Date: Tue, 26 Nov 2024 17:10:14 +0300 Subject: [PATCH 07/11] changed ColumnsValues access to private --- src/main/scala/com/ringcentral/cassandra4io/cql/package.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala b/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala index 89b71bb..cfea36d 100644 --- a/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala +++ b/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala @@ -325,8 +325,8 @@ package object cql { } } - trait ColumnsValues[T] extends Columns[T] with Values[T] - object ColumnsValues { + 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] { From a5c8fe758aeed0bba25477be4c704141ca27834e Mon Sep 17 00:00:00 2001 From: Aleksandr Shabalin Date: Wed, 27 Nov 2024 12:33:52 +0300 Subject: [PATCH 08/11] - renamed KeyEqualsTo to EqualsTo - updated doc --- .scalafmt.conf | 2 +- README.md | 49 +++++++++++++------ .../cassandra4io/cql/CqlSuite.scala | 8 +-- .../cassandra4io/cql/package.scala | 28 +++++------ 4 files changed, 52 insertions(+), 35 deletions(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index 3498245..c3542ef 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = "3.7.14" +version = "2.7.5" maxColumn = 120 align = most continuationIndent.defnSite = 2 diff --git a/README.md b/README.md index 4509504..586af55 100644 --- a/README.md +++ b/README.md @@ -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) + updateAlternative <- 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 // updateAlternative(value).execute.void + override def update(key: Key, data: Data): F[Unit] = updateQuery(data, key).execute.void + override def get(key: Key) = select(id).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 `pk = ?, ck = ?, data = ?, meta_data = ?` and `${EqualsTo[Key]}` expands to `pk = ? and ck = ? and data = ? and meta_data = ?`. +Latter three types adjust query type as well for being able to bind corresponding values ### Handling optional fields (`null`) diff --git a/src/it/scala/com/ringcentral/cassandra4io/cql/CqlSuite.scala b/src/it/scala/com/ringcentral/cassandra4io/cql/CqlSuite.scala index e2a0124..f2fc107 100644 --- a/src/it/scala/com/ringcentral/cassandra4io/cql/CqlSuite.scala +++ b/src/it/scala/com/ringcentral/cassandra4io/cql/CqlSuite.scala @@ -7,7 +7,7 @@ import com.ringcentral.cassandra4io.CassandraTestsSharedInstances import fs2.Stream import weaver._ -import java.time.{Duration, LocalDate, LocalTime} +import java.time.{ Duration, LocalDate, LocalTime } import java.util.UUID import java.util.concurrent.atomic.AtomicInteger @@ -202,7 +202,7 @@ trait CqlSuite { 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 ${KeyEqualsTo[Key]}" + cqlt"SELECT ${Columns[Table]} FROM ${Const("test_data_interpolated")} WHERE ${EqualsTo[Key]}" .as[Table] val data1 = Table(1, "projection-1", "data-1", 1, 1732547921580L) @@ -224,9 +224,9 @@ trait CqlSuite { 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 ${KeyEqualsTo[Key]}" + 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 ${KeyEqualsTo[Key]}" + cqlt"SELECT ${Columns[Data]} FROM ${Const("test_data_interpolated")} WHERE ${EqualsTo[Key]}" .as[Data] val data1 = Data("data-1", 1, 1732547921580L) diff --git a/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala b/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala index cfea36d..a49ddea 100644 --- a/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala +++ b/src/main/scala/com/ringcentral/cassandra4io/cql/package.scala @@ -126,17 +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 (((restriction: KeyEqualsTo[_]) :: tail, builder), 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) => + case (((assignment: Assignment[_]) :: tail, builder), part) => (tail, builder.appendAll(part).appendAll(assignment.keys.map(key => s"${key} = ?").mkString(", "))) - case (((columns: Columns[_]) :: tail, builder), part) => + case (((columns: Columns[_]) :: tail, builder), part) => (tail, builder.appendAll(part).appendAll(columns.keys.mkString(", "))) - case (((values: Values[_]) :: tail, builder), part) => + 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)) + case ((_ :: tail, builder), part) => (tail, builder.appendAll(part).appendAll("?")) + case ((HNil, builder), part) => (HNil, builder.appendAll(part)) } ._2 .toString(), @@ -289,27 +289,27 @@ package object cql { } case class Const(fragment: String) - trait Columns[T] { + trait Columns[T] { def keys: List[String] } - object Columns { + object Columns { def apply[T: ColumnsValues]: Columns[T] = new Columns[T] { override def keys: List[String] = ColumnsValues[T].keys } } - trait Values[T] { + trait Values[T] { def size: Int def binder: Binder[T] } - object Values { + 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 KeyEqualsTo[T] extends Columns[T] with Values[T] - object KeyEqualsTo { - def apply[T: ColumnsValues]: KeyEqualsTo[T] = new KeyEqualsTo[T] { + 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 From 3495f230c6508c65de95eb65b028716f9ce12d3a Mon Sep 17 00:00:00 2001 From: Aleksandr Shabalin Date: Wed, 27 Nov 2024 13:01:33 +0300 Subject: [PATCH 09/11] - updated doc --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 586af55..de79c9f 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ object Dao { value.metaData ).execute.void // updateAlternative(value).execute.void override def update(key: Key, data: Data): F[Unit] = updateQuery(data, key).execute.void - override def get(key: Key) = select(id).config(_.setExecutionProfileName("default")).select.head.compile.last + override def get(key: Key) = select(key).config(_.setExecutionProfileName("default")).select.head.compile.last } } ``` From 58a1e7886358694914d4a6e6b54cbfb8408e8856 Mon Sep 17 00:00:00 2001 From: Aleksandr Shabalin Date: Wed, 27 Nov 2024 13:30:06 +0300 Subject: [PATCH 10/11] - updated doc --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index de79c9f..7573343 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,7 @@ object Dao { } } ``` -As you can see `${Columns[Model]}` expands to `pk, ck, data, meta_data`, `${Values[Model]}` to `?, ?, ?, ?`, `${Assignment[Data]}` to `pk = ?, ck = ?, data = ?, meta_data = ?` and `${EqualsTo[Key]}` expands to `pk = ? and ck = ? and data = ? and meta_data = ?`. +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`) From b03e9ed5766d5ec3856146653526168fb086aee7 Mon Sep 17 00:00:00 2001 From: Aleksandr Shabalin Date: Wed, 27 Nov 2024 13:33:07 +0300 Subject: [PATCH 11/11] - updated doc --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7573343..33078f8 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ object Dao { def apply[F[_] : Async](session: CassandraSession[F]) = for { insert <- insertQuery.prepare(session) update <- updateQuery.prepare(session) - updateAlternative <- insertQueryAlternative.prepare(session) + insertAlternative <- insertQueryAlternative.prepare(session) select <- selectQuery.prepare(session) } yield new Dao[F] { override def put(value: Model) = insert( @@ -123,7 +123,7 @@ object Dao { value.ck, value.data, value.metaData - ).execute.void // updateAlternative(value).execute.void + ).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 }