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

Collections support #758

Merged
merged 16 commits into from
May 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,23 @@ ctx.run(q)
// SELECT p.id, p.name, p.age FROM Person p WHERE p.name like '%John%'
```

SQL-specific encoding
---------------------

**Arrays**

Quill provides SQL Arrays support. In Scala we represent them as any collection that implements `Seq`:
```
import java.util.Date

case class Person(id: Int, phones: List[String], cards: Vector[Int], dates: Seq[Date])

ctx.run(query[Person])
// SELECT x.id, x.phones, x.cards, x.dates FROM Person x
```
Note that not all drivers/databases provides such feature hence only `PostgresJdbcContext` and
`PostgresAsyncContext` support SQL Arrays.

Cassandra-specific operations
-----------------------------

Expand Down Expand Up @@ -815,6 +832,21 @@ ctx.run(q)
// DELETE p.age FROM Person
```

Cassandra-specific encoding
---------------------------

**Collections**

Quill provides List, Set and Map encoding:
```
import java.util.Date

case class Person(id: Int, phones: Set[String], cards: List[Int], dates: Map[Date, Boolean])

ctx.run(query[Person])
// SELECT id, phones, cards, dates FROM Person
```

Dynamic queries
---------------

Expand Down Expand Up @@ -935,6 +967,7 @@ import java.util.UUID
implicit val encodeUUID = MappedEncoding[UUID, String](_.toString)
implicit val decodeUUID = MappedEncoding[String, UUID](UUID.fromString(_))
```
Note that can it be also used to provide mapping for element types of collection (SQL Arrays or Cassandra Collections).

Raw Encoding
------------
Expand All @@ -954,6 +987,11 @@ trait UUIDEncodingExample {
implicit val uuidEncoder: Encoder[UUID] =
encoder(java.sql.Types.OTHER, (index, value, row) =>
row.setObject(index, value, java.sql.Types.OTHER)) // database-specific implementation

// Only for postgres
implicit def arrayUUIDEncoder[Col <: Seq[UUID]]: Encoder[Col] = arrayRawEncoder[UUID, Col]("uuid")
implicit def arrayUUIDDecoder[Col <: Seq[UUID]](implicit bf: CBF[UUID, Col]): Decoder[Col] =
arrayRawDecoder[UUID, Col]
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import com.github.mauricio.async.db.{ RowData, QueryResult => DBQueryResult }
import com.github.mauricio.async.db.pool.PartitionedConnectionPool
import com.github.mauricio.async.db.postgresql.PostgreSQLConnection
import com.typesafe.config.Config
import io.getquill.context.async.{ AsyncContext, UUIDObjectEncoding }
import io.getquill.context.async.{ ArrayDecoders, ArrayEncoders, AsyncContext, UUIDObjectEncoding }
import io.getquill.util.LoadConfig

class PostgresAsyncContext[N <: NamingStrategy](pool: PartitionedConnectionPool[PostgreSQLConnection])
extends AsyncContext[PostgresDialect, N, PostgreSQLConnection](pool) with UUIDObjectEncoding {
extends AsyncContext[PostgresDialect, N, PostgreSQLConnection](pool)
with ArrayEncoders
with ArrayDecoders
with UUIDObjectEncoding {

def this(config: PostgresAsyncContextConfig) = this(config.pool)
def this(config: Config) = this(PostgresAsyncContextConfig(config))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.getquill.context.async

import java.time.LocalDate
import java.util.Date

import io.getquill.PostgresAsyncContext
import io.getquill.context.sql.encoding.ArrayEncoding
import io.getquill.util.Messages.fail
import org.joda.time.{ LocalDate => JodaLocalDate, LocalDateTime => JodaLocalDateTime }

import scala.reflect.ClassTag

trait ArrayDecoders extends ArrayEncoding {
self: PostgresAsyncContext[_] =>

implicit def arrayStringDecoder[Col <: Seq[String]](implicit bf: CBF[String, Col]): Decoder[Col] = arrayRawEncoder[String, Col]
implicit def arrayBigDecimalDecoder[Col <: Seq[BigDecimal]](implicit bf: CBF[BigDecimal, Col]): Decoder[Col] = arrayRawEncoder[BigDecimal, Col]
implicit def arrayBooleanDecoder[Col <: Seq[Boolean]](implicit bf: CBF[Boolean, Col]): Decoder[Col] = arrayRawEncoder[Boolean, Col]
implicit def arrayByteDecoder[Col <: Seq[Byte]](implicit bf: CBF[Byte, Col]): Decoder[Col] = arrayDecoder[Short, Byte, Col](_.toByte)
implicit def arrayShortDecoder[Col <: Seq[Short]](implicit bf: CBF[Short, Col]): Decoder[Col] = arrayRawEncoder[Short, Col]
implicit def arrayIntDecoder[Col <: Seq[Index]](implicit bf: CBF[Index, Col]): Decoder[Col] = arrayRawEncoder[Index, Col]
implicit def arrayLongDecoder[Col <: Seq[Long]](implicit bf: CBF[Long, Col]): Decoder[Col] = arrayRawEncoder[Long, Col]
implicit def arrayFloatDecoder[Col <: Seq[Float]](implicit bf: CBF[Float, Col]): Decoder[Col] = arrayDecoder[Double, Float, Col](_.toFloat)
implicit def arrayDoubleDecoder[Col <: Seq[Double]](implicit bf: CBF[Double, Col]): Decoder[Col] = arrayRawEncoder[Double, Col]
implicit def arrayDateDecoder[Col <: Seq[Date]](implicit bf: CBF[Date, Col]): Decoder[Col] = arrayDecoder[JodaLocalDateTime, Date, Col](_.toDate)
implicit def arrayLocalDateTimeJodaDecoder[Col <: Seq[JodaLocalDateTime]](implicit bf: CBF[JodaLocalDateTime, Col]): Decoder[Col] = arrayRawEncoder[JodaLocalDateTime, Col]
implicit def arrayLocalDateJodaDecoder[Col <: Seq[JodaLocalDate]](implicit bf: CBF[JodaLocalDate, Col]): Decoder[Col] = arrayRawEncoder[JodaLocalDate, Col]
implicit def arrayLocalDateDecoder[Col <: Seq[LocalDate]](implicit bf: CBF[LocalDate, Col]): Decoder[Col] =
arrayDecoder[JodaLocalDate, LocalDate, Col](d => LocalDate.of(d.getYear, d.getMonthOfYear, d.getDayOfMonth))

def arrayDecoder[I, O, Col <: Seq[O]](mapper: I => O)(implicit bf: CBF[O, Col], iTag: ClassTag[I], oTag: ClassTag[O]): Decoder[Col] =
AsyncDecoder[Col](SqlTypes.ARRAY)(new BaseDecoder[Col] {
def apply(index: Index, row: ResultRow): Col = {
row(index) match {
case seq: IndexedSeq[Any] => seq.foldLeft(bf()) {
case (b, x: I) => b += mapper(x)
case (_, x) => fail(s"Array at index $index contains element of ${x.getClass.getCanonicalName}, but expected $iTag")
}.result()
case value => fail(
s"Value '$value' at index $index is not an array so it cannot be decoded to collection of $oTag"
)
}
}
})

def arrayRawEncoder[T: ClassTag, Col <: Seq[T]](implicit bf: CBF[T, Col]): Decoder[Col] =
arrayDecoder[T, T, Col](identity)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.getquill.context.async

import java.time.LocalDate
import java.util.Date

import io.getquill.PostgresAsyncContext
import io.getquill.context.sql.encoding.ArrayEncoding
import org.joda.time.{ LocalDate => JodaLocalDate, LocalDateTime => JodaLocalDateTime }

trait ArrayEncoders extends ArrayEncoding {
self: PostgresAsyncContext[_] =>

implicit def arrayStringEncoder[Col <: Seq[String]]: Encoder[Col] = arrayRawEncoder[String, Col]
implicit def arrayBigDecimalEncoder[Col <: Seq[BigDecimal]]: Encoder[Col] = arrayRawEncoder[BigDecimal, Col]
implicit def arrayBooleanEncoder[Col <: Seq[Boolean]]: Encoder[Col] = arrayRawEncoder[Boolean, Col]
implicit def arrayByteEncoder[Col <: Seq[Byte]]: Encoder[Col] = arrayRawEncoder[Byte, Col]
implicit def arrayShortEncoder[Col <: Seq[Short]]: Encoder[Col] = arrayRawEncoder[Short, Col]
implicit def arrayIntEncoder[Col <: Seq[Index]]: Encoder[Col] = arrayRawEncoder[Index, Col]
implicit def arrayLongEncoder[Col <: Seq[Long]]: Encoder[Col] = arrayRawEncoder[Long, Col]
implicit def arrayFloatEncoder[Col <: Seq[Float]]: Encoder[Col] = arrayRawEncoder[Float, Col]
implicit def arrayDoubleEncoder[Col <: Seq[Double]]: Encoder[Col] = arrayRawEncoder[Double, Col]
implicit def arrayDateEncoder[Col <: Seq[Date]]: Encoder[Col] = arrayRawEncoder[Date, Col]
implicit def arrayLocalDateTimeJodaEncoder[Col <: Seq[JodaLocalDateTime]]: Encoder[Col] = arrayRawEncoder[JodaLocalDateTime, Col]
implicit def arrayLocalDateJodaEncoder[Col <: Seq[JodaLocalDate]]: Encoder[Col] = arrayRawEncoder[JodaLocalDate, Col]
implicit def arrayLocalDateEncoder[Col <: Seq[LocalDate]]: Encoder[Col] = arrayRawEncoder[LocalDate, Col]

def arrayEncoder[T, Col <: Seq[T]](mapper: T => Any): Encoder[Col] =
encoder[Col]((col: Col) => col.toIndexedSeq.map(mapper), SqlTypes.ARRAY)

def arrayRawEncoder[T, Col <: Seq[T]]: Encoder[Col] = arrayEncoder[T, Col](identity)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package io.getquill.context.async.postgres

import java.time.LocalDate

import io.getquill.context.sql.encoding.ArrayEncodingBaseSpec
import org.joda.time.{ LocalDate => JodaLocalDate, LocalDateTime => JodaLocalDateTime }

import scala.concurrent.ExecutionContext.Implicits.global

class ArrayAsyncEncodingSpec extends ArrayEncodingBaseSpec {
val ctx = testContext
import ctx._

val q = quote(query[ArraysTestEntity])

"Support all sql base types and `Traversable` implementers" in {
await(ctx.run(q.insert(lift(e))))
val actual = await(ctx.run(q)).head
actual mustEqual e
baseEntityDeepCheck(actual, e)
}

"Joda times" in {
case class JodaTimes(timestamps: Seq[JodaLocalDateTime], dates: Seq[JodaLocalDate])
val jE = JodaTimes(Seq(JodaLocalDateTime.now()), Seq(JodaLocalDate.now()))
val jQ = quote(querySchema[JodaTimes]("ArraysTestEntity"))
await(ctx.run(jQ.insert(lift(jE))))
val actual = await(ctx.run(jQ)).head
actual.timestamps mustBe jE.timestamps
actual.dates mustBe jE.dates
}

"Support Traversable encoding basing on MappedEncoding" in {
val wrapQ = quote(querySchema[WrapEntity]("ArraysTestEntity"))
await(ctx.run(wrapQ.insert(lift(wrapE))))
await(ctx.run(wrapQ)).head mustBe wrapE
}

"Catch invalid decoders" in {
val newCtx = new TestContext {
// avoid transforming from org.joda.time.LocalDate to java.time.LocalDate
override implicit def arrayLocalDateDecoder[Col <: Seq[LocalDate]](implicit bf: CBF[LocalDate, Col]): Decoder[Col] =
arrayDecoder[LocalDate, LocalDate, Col](identity)
}
import newCtx._
await(newCtx.run(query[ArraysTestEntity].insert(lift(e))))
intercept[IllegalStateException] {
await(newCtx.run(query[ArraysTestEntity])).head mustBe e
}
newCtx.close()
}

"Arrays in where clause" in {
await(ctx.run(q.insert(lift(e))))
val actual1 = await(ctx.run(q.filter(_.texts == lift(List("test")))))
val actual2 = await(ctx.run(q.filter(_.texts == lift(List("test2")))))
actual1 mustEqual List(e)
actual2 mustEqual List()
}

override protected def beforeEach(): Unit = {
await(ctx.run(q.delete))
()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,13 @@ package io.getquill.context.async.postgres
import io.getquill.context.sql.{ TestDecoders, TestEncoders }
import io.getquill.{ Literal, PostgresAsyncContext, TestEntities }

class TestContext
extends PostgresAsyncContext[Literal]("testPostgresDB") with TestEntities with TestEncoders with TestDecoders
import scala.concurrent.{ Await, Future }
import scala.concurrent.duration.Duration

class TestContext extends PostgresAsyncContext[Literal]("testPostgresDB")
with TestEntities
with TestEncoders
with TestDecoders {

def await[T](f: Future[T]): T = Await.result(f, Duration.Inf)
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
package io.getquill

import io.getquill.context.cassandra.CassandraContext
import io.getquill.context.cassandra.CqlIdiom
import io.getquill.context.cassandra.encoding.CassandraMapper
import io.getquill.context.cassandra.{ CassandraContext, CqlIdiom }

import scala.reflect.ClassTag

class CassandraMirrorContextWithQueryProbing extends CassandraMirrorContext with QueryProbing

class CassandraMirrorContext[Naming <: NamingStrategy]
extends MirrorContext[CqlIdiom, Naming] with CassandraContext[Naming]
extends MirrorContext[CqlIdiom, Naming] with CassandraContext[Naming] {

implicit def listDecoder[T, Cas: ClassTag](implicit mapper: CassandraMapper[Cas, T]): Decoder[List[T]] = decoderUnsafe[List[T]]
implicit def setDecoder[T, Cas: ClassTag](implicit mapper: CassandraMapper[Cas, T]): Decoder[Set[T]] = decoderUnsafe[Set[T]]
implicit def mapDecoder[K, V, KCas: ClassTag, VCas: ClassTag](
implicit
keyMapper: CassandraMapper[KCas, K],
valMapper: CassandraMapper[VCas, V]
): Decoder[Map[K, V]] = decoderUnsafe[Map[K, V]]

implicit def listEncoder[T, Cas](implicit mapper: CassandraMapper[T, Cas]): Encoder[List[T]] = encoder[List[T]]
implicit def setEncoder[T, Cas](implicit mapper: CassandraMapper[T, Cas]): Encoder[Set[T]] = encoder[Set[T]]
implicit def mapEncoder[K, V, KCas, VCas](
implicit
keyMapper: CassandraMapper[K, KCas],
valMapper: CassandraMapper[V, VCas]
): Encoder[Map[K, V]] = encoder[Map[K, V]]
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ import java.util.{ Date, UUID }

import io.getquill.NamingStrategy
import io.getquill.context.Context
import io.getquill.context.cassandra.encoding.{ CassandraMapper, CassandraMapperConversions, CassandraTypes }

import scala.reflect.ClassTag

trait CassandraContext[N <: NamingStrategy]
extends Context[CqlIdiom, N]
with CassandraMapperConversions
with CassandraTypes
with Ops {

implicit def optionDecoder[T](implicit d: Decoder[T]): Decoder[Option[T]]
Expand All @@ -33,4 +38,20 @@ trait CassandraContext[N <: NamingStrategy]
implicit val byteArrayEncoder: Encoder[Array[Byte]]
implicit val uuidEncoder: Encoder[UUID]
implicit val dateEncoder: Encoder[Date]

implicit def listDecoder[T, Cas: ClassTag](implicit mapper: CassandraMapper[Cas, T]): Decoder[List[T]]
implicit def setDecoder[T, Cas: ClassTag](implicit mapper: CassandraMapper[Cas, T]): Decoder[Set[T]]
implicit def mapDecoder[K, V, KCas: ClassTag, VCas: ClassTag](
implicit
keyMapper: CassandraMapper[KCas, K],
valMapper: CassandraMapper[VCas, V]
): Decoder[Map[K, V]]

implicit def listEncoder[T, Cas](implicit mapper: CassandraMapper[T, Cas]): Encoder[List[T]]
implicit def setEncoder[T, Cas](implicit mapper: CassandraMapper[T, Cas]): Encoder[Set[T]]
implicit def mapEncoder[K, V, KCas, VCas](
implicit
keyMapper: CassandraMapper[K, KCas],
valMapper: CassandraMapper[V, VCas]
): Encoder[Map[K, V]]
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
package io.getquill.context.cassandra

import scala.util.Try

import org.slf4j.LoggerFactory

import com.datastax.driver.core.BoundStatement
import com.datastax.driver.core.Row
import com.typesafe.scalalogging.Logger

import io.getquill.NamingStrategy
import io.getquill.context.cassandra.encoding.Decoders
import io.getquill.context.cassandra.encoding.Encoders
import io.getquill.context.cassandra.encoding.{ CassandraTypes, Decoders, Encoders }
import io.getquill.util.Messages.fail
import com.datastax.driver.core.Cluster

Expand All @@ -21,7 +17,8 @@ abstract class CassandraSessionContext[N <: NamingStrategy](
)
extends CassandraContext[N]
with Encoders
with Decoders {
with Decoders
with CassandraTypes {

override type PrepareRow = BoundStatement
override type ResultRow = Row
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.getquill.context.cassandra.encoding

/**
* Developers API.
*
* End-users should rely on MappedEncoding since it's more general.
*/
case class CassandraMapper[I, O](f: I => O)
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package io.getquill.context.cassandra.encoding

import io.getquill.MappedEncoding

trait CassandraMapperConversions extends CassandraMapperConversionsLowPriorityImplicits {

implicit def cassandraIdentityMapper[Cas](implicit cas: CassandraType[Cas]): CassandraMapper[Cas, Cas] =
CassandraMapper(identity)

implicit def cassandraMapperEncode[T, Cas](
implicit
m: MappedEncoding[T, Cas],
cas: CassandraType[Cas]
): CassandraMapper[T, Cas] = CassandraMapper(m.f)

implicit def cassandraMapperDecode[T, Cas](
implicit
m: MappedEncoding[Cas, T],
cas: CassandraType[Cas]
): CassandraMapper[Cas, T] = CassandraMapper(m.f)
}

trait CassandraMapperConversionsLowPriorityImplicits {

implicit def cassandraMapperEncodeRec[I, O, Cas](
implicit
me: MappedEncoding[I, O],
cm: CassandraMapper[O, Cas]
): CassandraMapper[I, Cas] = CassandraMapper(me.f.andThen(cm.f))

implicit def cassandraMapperDecodeRec[I, O, Cas](
implicit
m: MappedEncoding[I, O],
cm: CassandraMapper[Cas, I]
): CassandraMapper[Cas, O] = CassandraMapper(cm.f.andThen(m.f))
}
Loading