Skip to content

Commit

Permalink
Add support of 'contains' on cassandra collections
Browse files Browse the repository at this point in the history
  • Loading branch information
mentegy committed Sep 11, 2017
1 parent 40b72e3 commit 271d751
Show file tree
Hide file tree
Showing 27 changed files with 368 additions and 215 deletions.
59 changes: 45 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -493,15 +493,15 @@ val q = quote {
}

ctx.run(q)
// SELECT p.id, p.name, p.age, c.personId, c.phone
// SELECT p.id, p.name, p.age, c.personId, c.phone
// FROM Person p INNER JOIN Contact c ON c.personId = p.id

val q = quote {
query[Person].leftJoin(query[Contact]).on((p, c) => c.personId == p.id)
}

ctx.run(q)
// SELECT p.id, p.name, p.age, c.personId, c.phone
// SELECT p.id, p.name, p.age, c.personId, c.phone
// FROM Person p LEFT JOIN Contact c ON c.personId = p.id

```
Expand Down Expand Up @@ -533,7 +533,7 @@ val qNested = quote {

ctx.run(qFlat)
ctx.run(qNested)
// SELECT p.id, p.name, p.age, e.id, e.personId, e.name, c.id, c.phone
// SELECT p.id, p.name, p.age, e.id, e.personId, e.name, c.id, c.phone
// FROM Person p INNER JOIN Employer e ON p.id = e.personId LEFT JOIN Contact c ON c.personId = p.id
```

Expand Down Expand Up @@ -705,15 +705,30 @@ ctx.run(query[Book])
Note that not all drivers/databases provides such feature hence only `PostgresJdbcContext` and
`PostgresAsyncContext` support SQL Arrays.

## Cassandra-specific operations

The cassandra context also provides a few additional operations:
## Cassandra-specific encoding

```scala
val ctx = new CassandraMirrorContext
import ctx._
```

### Collections

The cassandra context provides List, Set and Map encoding:

```scala

case class Book(id: Int, notes: Set[String], pages: List[Int], history: Map[Int, Boolean])

ctx.run(query[Book])
// SELECT id, notes, pages, history FROM Book
```

## Cassandra-specific operations

The cassandra context also provides a few additional operations:

### allowFiltering
```scala
val q = quote {
Expand Down Expand Up @@ -822,18 +837,34 @@ ctx.run(q)
// DELETE p.age FROM Person
```

## Cassandra-specific encoding

### Collections

Quill provides List, Set and Map encoding:
### list.contains / set.contains
requires `allowFiltering`
```scala
import java.util.Date
val q = quote {
query[Book].filter(p => p.pages.contains(25)).allowFiltering
}
ctx.run(q)
// SELECT id, notes, pages, history FROM Book WHERE pages CONTAINS 25 ALLOW FILTERING
```

case class Book(id: Int, notes: Set[String], pages: List[Int], history: Map[Date, Boolean])
### map.contains
requires `allowFiltering`
```scala
val q = quote {
query[Book].filter(p => p.history.contains(12)).allowFiltering
}
ctx.run(q)
// SELECT id, notes, pages, history FROM book WHERE history CONTAINS 12 ALLOW FILTERING
```

ctx.run(query[Book])
// SELECT id, notes, pages, history FROM Book
### map.containsValue
requires `allowFiltering`
```scala
val q = quote {
query[Book].filter(p => p.history.containsValue(true)).allowFiltering
}
ctx.run(q)
// SELECT id, notes, pages, history FROM book WHERE history CONTAINS true ALLOW FILTERING
```

## Dynamic queries
Expand Down
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ lazy val `quill-finagle-mysql` =
.settings(
fork in Test := true,
libraryDependencies ++= Seq(
"com.twitter" %% "finagle-mysql" % "7.1.0"
"com.twitter" %% "finagle-mysql" % "7.0.0"
)
)
.dependsOn(`quill-sql-jvm` % "compile->compile;test->test")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,21 @@ class CassandraAsyncContext[N <: NamingStrategy](
override type RunActionResult = Future[Unit]
override type RunBatchActionResult = Future[Unit]

def executeQuery[T](cql: String, prepare: Prepare = identityPrepare, extractor: Extractor[T] = identityExtractor)(implicit ec: ExecutionContext): Future[List[T]] =
Future {
val (params, bs) = prepare(super.prepare(cql))
logger.logQuery(cql, params)
session.executeAsync(bs)
.map(_.all.asScala.toList.map(extractor))
}.flatMap(identity _)
def executeQuery[T](cql: String, prepare: Prepare = identityPrepare, extractor: Extractor[T] = identityExtractor)(implicit ec: ExecutionContext): Future[List[T]] = {
val (params, bs) = prepare(super.prepare(cql))
logger.logQuery(cql, params)
session.executeAsync(bs)
.map(_.all.asScala.toList.map(extractor))
}

def executeQuerySingle[T](cql: String, prepare: Prepare = identityPrepare, extractor: Extractor[T] = identityExtractor)(implicit ec: ExecutionContext): Future[T] =
executeQuery(cql, prepare, extractor).map(handleSingleResult)

def executeAction[T](cql: String, prepare: Prepare = identityPrepare)(implicit ec: ExecutionContext): Future[Unit] =
Future {
val (params, bs) = prepare(super.prepare(cql))
logger.logQuery(cql, params)
session.executeAsync(bs).map(_ => ())
}.flatMap(identity _)
def executeAction[T](cql: String, prepare: Prepare = identityPrepare)(implicit ec: ExecutionContext): Future[Unit] = {
val (params, bs) = prepare(super.prepare(cql))
logger.logQuery(cql, params)
session.executeAsync(bs).map(_ => ())
}

def executeBatchAction(groups: List[BatchGroup])(implicit ec: ExecutionContext): Future[Unit] =
Future.sequence {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.getquill.context.cassandra

import io.getquill.ast._
import io.getquill.ast.{ TraversableOperation, _ }
import io.getquill.NamingStrategy
import io.getquill.util.Messages.fail
import io.getquill.idiom.Idiom
Expand Down Expand Up @@ -29,16 +29,17 @@ trait CqlIdiom extends Idiom {
Tokenizer[Ast] {
case Aggregation(AggregationOperator.`size`, Constant(1)) =>
"COUNT(1)".token
case a: Query => a.token
case a: Operation => a.token
case a: Action => a.token
case a: Ident => a.token
case a: Property => a.token
case a: Value => a.token
case a: Function => a.body.token
case a: Infix => a.token
case a: Lift => a.token
case a: Assignment => a.token
case a: Query => a.token
case a: Operation => a.token
case a: Action => a.token
case a: Ident => a.token
case a: Property => a.token
case a: Value => a.token
case a: Function => a.body.token
case a: Infix => a.token
case a: Lift => a.token
case a: Assignment => a.token
case a: TraversableOperation => a.token
case a @ (
_: Function | _: FunctionApply | _: Dynamic | _: OptionOperation | _: Block |
_: Val | _: Ordering | _: QuotedReference | _: If
Expand Down Expand Up @@ -181,4 +182,11 @@ trait CqlIdiom extends Idiom {
implicit def entityTokenizer(implicit strategy: NamingStrategy): Tokenizer[Entity] = Tokenizer[Entity] {
case Entity(name, properties) => strategy.table(name).token
}

implicit def traversableTokenizer(implicit strategy: NamingStrategy): Tokenizer[TraversableOperation] =
Tokenizer[TraversableOperation] {
case MapContains(ast, body) => stmt"${ast.token} CONTAINS KEY ${body.token}"
case SetContains(ast, body) => stmt"${ast.token} CONTAINS ${body.token}"
case ListContains(ast, body) => stmt"${ast.token} CONTAINS ${body.token}"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,8 @@ trait Ops {
def ifCond(cond: T => Boolean) =
quote(infix"$q IF $cond".as[Action[T]])
}

implicit class MapOps[K, V](map: Map[K, V]) {
def containsValue(value: V) = quote(infix"$map CONTAINS $value".as[Boolean])
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package io.getquill.context.cassandra

import io.getquill._
import scala.concurrent.ExecutionContext.Implicits.{ global => ec }
import scala.util.Try

class CassandraContextSpec extends Spec {

Expand All @@ -25,14 +24,4 @@ class CassandraContextSpec extends Spec {
testSyncDB.run(update) mustEqual (())
}
}

"async - returns failed future if prepare fails" in {
import testAsyncDB._
case class InvalidTestEntity(id: Int, s: String, i: Int, l: Long, o: Int)
val update = quote {
query[InvalidTestEntity].filter(_.id == lift(1)).update(_.i -> lift(1))
}
val fut = testAsyncDB.run(update)
Try(await(fut)).isFailure mustEqual true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.getquill.context.cassandra

import io.getquill.TestEntities

trait CassandraTestEntities extends TestEntities {
this: CassandraContext[_] =>

case class MapFrozen(id: Map[Int, Boolean])
val mapFroz = quote(query[MapFrozen])

case class SetFrozen(id: Set[Int])
val setFroz = quote(query[SetFrozen])

case class ListFrozen(id: List[Int])
val listFroz = quote(query[ListFrozen])
}
Original file line number Diff line number Diff line change
Expand Up @@ -337,4 +337,19 @@ class CqlIdiomSpec extends Spec {
}
}
}

"collections operations" - {
"map.contains" in {
mirrorContext.run(mapFroz.filter(x => x.id.contains(1))).string mustEqual
"SELECT id FROM MapFrozen WHERE id CONTAINS KEY 1"
}
"set.contains" in {
mirrorContext.run(setFroz.filter(x => x.id.contains(2))).string mustEqual
"SELECT id FROM SetFrozen WHERE id CONTAINS 2"
}
"list.contains" in {
mirrorContext.run(listFroz.filter(x => x.id.contains(3))).string mustEqual
"SELECT id FROM ListFrozen WHERE id CONTAINS 3"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,18 @@ class ListsEncodingSpec extends CollectionsSpec {
.head.blobs.map(_.toList) mustBe e.blobs.map(_.toList)
}

"List in where clause" in {
case class ListFrozen(id: List[Int])
"List in where clause / contains" in {
val e = ListFrozen(List(1, 2))
val q = quote(query[ListFrozen])
ctx.run(q.insert(lift(e)))
ctx.run(q.filter(_.id == lift(List(1, 2)))) mustBe List(e)
ctx.run(q.filter(_.id == lift(List(1)))) mustBe Nil
ctx.run(listFroz.insert(lift(e)))
ctx.run(listFroz.filter(_.id == lift(List(1, 2)))) mustBe List(e)
ctx.run(listFroz.filter(_.id == lift(List(1)))) mustBe Nil

ctx.run(listFroz.filter(_.id.contains(2)).allowFiltering) mustBe List(e)
ctx.run(listFroz.filter(_.id.contains(3)).allowFiltering) mustBe Nil
}

override protected def beforeEach(): Unit = {
ctx.run(q.delete)
ctx.run(listFroz.delete)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,26 @@ class MapsEncodingSpec extends CollectionsSpec {
ctx.run(q.filter(_.id == 1)).head mustBe e
}

"Map in where clause" in {
case class MapFrozen(id: Map[Int, Boolean])
"Map in where clause / contains" in {
val e = MapFrozen(Map(1 -> true))
val q = quote(query[MapFrozen])
ctx.run(q.insert(lift(e)))
ctx.run(q.filter(_.id == lift(Map(1 -> true)))) mustBe List(e)
ctx.run(q.filter(_.id == lift(Map(1 -> false)))) mustBe List()
ctx.run(mapFroz.insert(lift(e)))
ctx.run(mapFroz.filter(_.id == lift(Map(1 -> true)))) mustBe List(e)
ctx.run(mapFroz.filter(_.id == lift(Map(1 -> false)))) mustBe Nil

ctx.run(mapFroz.filter(_.id.contains(1)).allowFiltering) mustBe List(e)
ctx.run(mapFroz.filter(_.id.contains(2)).allowFiltering) mustBe Nil
}

"Map.containsValue" in {
val e = MapFrozen(Map(1 -> true))
ctx.run(mapFroz.insert(lift(e)))

ctx.run(mapFroz.filter(_.id.containsValue(true)).allowFiltering) mustBe List(e)
ctx.run(mapFroz.filter(_.id.containsValue(false)).allowFiltering) mustBe Nil
}

override protected def beforeEach(): Unit = {
ctx.run(q.delete)
ctx.run(mapFroz.delete)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,14 @@ class SetsEncodingSpec extends CollectionsSpec {
}

"Set in where clause" in {
case class SetFrozen(id: Set[Int])
val e = SetFrozen(Set(1, 2))
val q = quote(query[SetFrozen])
ctx.run(q.insert(lift(e)))
ctx.run(q.filter(_.id == lift(Set(1, 2)))) mustBe List(e)
ctx.run(q.filter(_.id == lift(Set(1)))) mustBe List()
ctx.run(setFroz.insert(lift(e)))
ctx.run(setFroz.filter(_.id == lift(Set(1, 2)))) mustBe List(e)
ctx.run(setFroz.filter(_.id == lift(Set(1)))) mustBe List()
}

override protected def beforeEach(): Unit = {
ctx.run(q.delete)
ctx.run(setFroz.delete)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,11 @@ class CassandraOpsSpec extends Spec {
"TRUNCATE TestEntity IF EXISTS"
}
}

"collection" - {
"map.containsValue" in {
mirrorContext.run(mapFroz.filter(x => x.id.containsValue(true))).string mustEqual
"SELECT id FROM MapFrozen WHERE id CONTAINS true"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import io.getquill.CassandraStreamContext

package object cassandra {

lazy val mirrorContext = new CassandraMirrorContext with TestEntities
lazy val mirrorContext = new CassandraMirrorContext with CassandraTestEntities

lazy val testSyncDB = new CassandraSyncContext[Literal]("testSyncDB") with TestEntities
lazy val testSyncDB = new CassandraSyncContext[Literal]("testSyncDB") with CassandraTestEntities

lazy val testAsyncDB = new CassandraAsyncContext[Literal]("testAsyncDB") with TestEntities
lazy val testAsyncDB = new CassandraAsyncContext[Literal]("testAsyncDB") with CassandraTestEntities

lazy val testStreamDB = new CassandraStreamContext[Literal]("testStreamDB") with TestEntities
lazy val testStreamDB = new CassandraStreamContext[Literal]("testStreamDB") with CassandraTestEntities

def await[T](f: Future[T]): T = Await.result(f, Duration.Inf)
}
Loading

0 comments on commit 271d751

Please sign in to comment.