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

Add support of 'contains' operation on Cassandra collections #813

Merged
merged 1 commit into from Sep 11, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
59 changes: 45 additions & 14 deletions README.md
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
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
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
@@ -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}"
}
}
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])
}
}
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
}
}
@@ -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])
}
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"
}
}
}
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)
}
}
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)
}
}
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)
}
}
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"
}
}
}
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)
}