Skip to content

Commit

Permalink
Merge pull request zio#957 from deusaquilus/master
Browse files Browse the repository at this point in the history
Ad-Hoc Tuple Support in Quotations
  • Loading branch information
fwbrasil committed Nov 15, 2017
2 parents cdaddbe + de121ee commit c634645
Show file tree
Hide file tree
Showing 36 changed files with 983 additions and 9 deletions.
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,54 @@ ctx.run(qNested)
// FROM Person p INNER JOIN Employer e ON p.id = e.personId LEFT JOIN Contact c ON c.personId = p.id
```

### Ad-Hoc Case Classes

Case Classes can also be used inside quotations as output values:

```scala
case class Person(id: Int, name: String, age: Int)
case class Contact(personId: Int, phone: String)
case class ReachablePerson(name:String, phone: String)

val q = quote {
for {
p <- query[Person] if(p.id == 999)
c <- query[Contact] if(c.personId == p.id)
} yield {
ReachablePerson(p.name, c.phone)
}
}

ctx.run(q)
// SELECT p.name, c.phone FROM Person p, Contact c WHERE (p.id = 999) AND (c.personId = p.id)
```

As well as in general:

```scala
case class IdFilter(id:Int)

val q = quote {
val idFilter = new IdFilter(999)
for {
p <- query[Person] if(p.id == idFilter.id)
c <- query[Contact] if(c.personId == p.id)
} yield {
ReachablePerson(p.name, c.phone)
}
}

ctx.run(q)
// SELECT p.name, c.phone FROM Person p, Contact c WHERE (p.id = 999) AND (c.personId = p.id)
```
***Note*** however that this functionality has the following restrictions:
1. The Ad-Hoc Case Class can only have one constructor with one set of parameters.
2. The Ad-Hoc Case Class must be constructed inside the quotation using one of the following methods:
1. Using the `new` keyword: `new Person("Joe", "Bloggs")`
2. Using a companion object's apply method: `Person("Joe", "Bloggs")`
3. Using a companion object's apply method explicitly: `Person.apply("Joe", "Bloggs")`
4. Any custom logic in a constructor/apply-method of a Ad-Hoc case class will not be invoked when it is 'constructed' inside a quotation. To construct an Ad-Hoc case class with custom logic inside a quotation, you can use a quoted method.

## Query probing

Query probing validates queries against the database at compile time, failing the compilation if it is not valid. The query validation does not alter the database state.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.getquill.context.async.mysql

import scala.concurrent.ExecutionContext.Implicits.{ global => ec }
import io.getquill.context.sql.CaseClassQuerySpec
import org.scalatest.Matchers._

class CaseClassQueryAsyncSpec extends CaseClassQuerySpec {

val context = testContext
import testContext._

override def beforeAll =
await {
testContext.transaction { implicit ec =>
for {
_ <- testContext.run(query[Contact].delete)
_ <- testContext.run(query[Address].delete)
_ <- testContext.run(liftQuery(peopleEntries).foreach(e => peopleInsert(e)))
_ <- testContext.run(liftQuery(addressEntries).foreach(e => addressInsert(e)))
} yield {}
}
}

"Example 1 - Single Case Class Mapping" in {
await(testContext.run(`Ex 1 CaseClass Record Output`)) should contain theSameElementsAs `Ex 1 CaseClass Record Output expected result`
}
"Example 1A - Single Case Class Mapping" in {
await(testContext.run(`Ex 1A CaseClass Record Output`)) should contain theSameElementsAs `Ex 1 CaseClass Record Output expected result`
}
"Example 1B - Single Case Class Mapping" in {
await(testContext.run(`Ex 1B CaseClass Record Output`)) should contain theSameElementsAs `Ex 1 CaseClass Record Output expected result`
}

"Example 2 - Single Record Mapped Join" in {
await(testContext.run(`Ex 2 Single-Record Join`)) should contain theSameElementsAs `Ex 2 Single-Record Join expected result`
}

"Example 3 - Inline Record as Filter" in {
await(testContext.run(`Ex 3 Inline Record Usage`)) should contain theSameElementsAs `Ex 3 Inline Record Usage exepected result`
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.getquill.context.async.postgres

import scala.concurrent.ExecutionContext.Implicits.{ global => ec }
import io.getquill.context.sql.CaseClassQuerySpec
import org.scalatest.Matchers._

class CaseClassQueryAsyncSpec extends CaseClassQuerySpec {

val context = testContext
import testContext._

override def beforeAll =
await {
testContext.transaction { implicit ec =>
for {
_ <- testContext.run(query[Contact].delete)
_ <- testContext.run(query[Address].delete)
_ <- testContext.run(liftQuery(peopleEntries).foreach(e => peopleInsert(e)))
_ <- testContext.run(liftQuery(addressEntries).foreach(e => addressInsert(e)))
} yield {}
}
}

"Example 1 - Single Case Class Mapping" in {
await(testContext.run(`Ex 1 CaseClass Record Output`)) should contain theSameElementsAs `Ex 1 CaseClass Record Output expected result`
}
"Example 1A - Single Case Class Mapping" in {
await(testContext.run(`Ex 1A CaseClass Record Output`)) should contain theSameElementsAs `Ex 1 CaseClass Record Output expected result`
}
"Example 1B - Single Case Class Mapping" in {
await(testContext.run(`Ex 1B CaseClass Record Output`)) should contain theSameElementsAs `Ex 1 CaseClass Record Output expected result`
}

"Example 2 - Single Record Mapped Join" in {
await(testContext.run(`Ex 2 Single-Record Join`)) should contain theSameElementsAs `Ex 2 Single-Record Join expected result`
}

"Example 3 - Inline Record as Filter" in {
await(testContext.run(`Ex 3 Inline Record Usage`)) should contain theSameElementsAs `Ex 3 Inline Record Usage exepected result`
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ trait CqlIdiom extends Idiom {
case Constant(()) => stmt"1"
case Constant(v) => stmt"${v.toString.token}"
case Tuple(values) => stmt"${values.token}"
case CaseClass(values) => stmt"${values.map(_._2).token}"
case NullValue => fail("Cql doesn't support null values.")
}

Expand Down
18 changes: 17 additions & 1 deletion quill-cassandra/src/test/cql/cassandra-schema.cql
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,20 @@ CREATE TYPE quill_test_2.name(
CREATE TABLE quill_test_2.with_udt(
id INT PRIMARY KEY,
name frozen <quill_test_2.Name>
);
);

CREATE TABLE Contact(
id INT PRIMARY KEY,
firstName VARCHAR,
lastName VARCHAR,
age INT,
addressFk INT,
extraInfo VARCHAR
);

CREATE TABLE Address(
id INT PRIMARY KEY,
street VARCHAR,
zip INT,
otherExtraInfo VARCHAR
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package io.getquill.context.cassandra

import io.getquill.Spec

class CaseClassQueryCassandraSpec extends Spec {

import testSyncDB._

case class Contact(id: Int, firstName: String, lastName: String, age: Int, addressFk: Int, extraInfo: String)
case class Address(id: Int, street: String, zip: Int, otherExtraInfo: String)

val peopleInsert =
quote((p: Contact) => query[Contact].insert(p))

val peopleEntries = List(
Contact(1, "Alex", "Jones", 60, 2, "foo"),
Contact(2, "Bert", "James", 55, 3, "bar"),
Contact(3, "Cora", "Jasper", 33, 3, "baz")
)

val addressInsert =
quote((c: Address) => query[Address].insert(c))

val addressEntries = List(
Address(1, "123 Fake Street", 11234, "something"),
Address(2, "456 Old Street", 45678, "something else"),
Address(3, "789 New Street", 89010, "another thing")
)

case class ContactSimplified(firstName: String, lastName: String, age: Int)
case class AddressableContact(firstName: String, lastName: String, age: Int, street: String, zip: Int)

val `Ex 1 CaseClass Record Output` = quote {
query[Contact].map(p => new ContactSimplified(p.firstName, p.lastName, p.age))
}

val `Ex 1 CaseClass Record Output expected result` = List(
ContactSimplified("Alex", "Jones", 60),
ContactSimplified("Bert", "James", 55),
ContactSimplified("Cora", "Jasper", 33)
)

case class FiltrationObject(idFilter: Int)

val `Ex 3 Inline Record Usage` = quote {
val filtrationObject = new FiltrationObject(1)
query[Contact].filter(p => p.id == filtrationObject.idFilter)
}

val `Ex 3 Inline Record Usage exepected result` = List(
new Contact(1, "Alex", "Jones", 60, 2, "foo")
)

override def beforeAll = {
testSyncDB.run(query[Contact].delete)
testSyncDB.run(query[Address].delete)
testSyncDB.run(liftQuery(peopleEntries).foreach(p => peopleInsert(p)))
testSyncDB.run(liftQuery(addressEntries).foreach(p => addressInsert(p)))
}

"Example 1 - Single Case Class Mapping" in {
testSyncDB.run(`Ex 1 CaseClass Record Output`) mustEqual `Ex 1 CaseClass Record Output expected result`
}

"Example 2 - Inline Record as Filter" in {
testSyncDB.run(`Ex 3 Inline Record Usage`) mustEqual `Ex 3 Inline Record Usage exepected result`
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,9 @@ class CqlIdiomSpec extends Spec {
"value" in {
implicitly[Tokenizer[Value]].token(Tuple(List(Ident("a")))) mustBe stmt"a"
}
"value in caseclass" in {
implicitly[Tokenizer[Value]].token(CaseClass(List(("value", Ident("a"))))) mustBe stmt"a"
}
"action" in {
val t = implicitly[Tokenizer[AstAction]]
intercept[IllegalStateException](t.token(null: AstAction))
Expand Down
1 change: 1 addition & 0 deletions quill-core/src/main/scala/io/getquill/MirrorIdiom.scala
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ class MirrorIdiom extends Idiom {
case Constant(v) => stmt"${v.toString.token}"
case NullValue => stmt"null"
case Tuple(values) => stmt"(${values.token})"
case CaseClass(values) => stmt"(${values.map(_._2).token})"
}

implicit val identTokenizer: Tokenizer[Ident] = Tokenizer[Ident] {
Expand Down
1 change: 1 addition & 0 deletions quill-core/src/main/scala/io/getquill/ast/Ast.scala
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ case class Constant(v: Any) extends Value
object NullValue extends Value

case class Tuple(values: List[Ast]) extends Value
case class CaseClass(values: List[(String, Ast)]) extends Value

//************************************************************

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,10 @@ trait StatefulTransformer[T] {
case Tuple(a) =>
val (at, att) = apply(a)(_.apply)
(Tuple(at), att)
case CaseClass(a) =>
val (keys, values) = a.unzip
val (at, att) = apply(values)(_.apply)
(CaseClass(keys.zip(at)), att)
}

def apply(e: Action): (Action, StatefulTransformer[T]) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ trait StatelessTransformer {
case e: Constant => e
case NullValue => NullValue
case Tuple(values) => Tuple(values.map(apply))
case CaseClass(tuples) => {
val (keys, values) = tuples.unzip
CaseClass(keys.zip(values.map(apply)))
}
}

def apply(e: Action): Action =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ case class BetaReduction(map: collection.Map[Ast, Ast])
case _ => apply(values(name.drop(1).toInt - 1))
}

case Property(CaseClass(tuples), name) =>
apply(tuples.toMap.apply(name))

case FunctionApply(Function(params, body), values) =>
val conflicts = values.flatMap(CollectAst.byType[Ident]).map { i =>
i -> Ident(s"tmp_${i.name}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,10 @@ trait Liftables {
}

implicit val valueLiftable: Liftable[Value] = Liftable[Value] {
case NullValue => q"$pack.NullValue"
case Constant(a) => q"$pack.Constant(${Literal(c.universe.Constant(a))})"
case Tuple(a) => q"$pack.Tuple($a)"
case NullValue => q"$pack.NullValue"
case Constant(a) => q"$pack.Constant(${Literal(c.universe.Constant(a))})"
case Tuple(a) => q"$pack.Tuple($a)"
case CaseClass(a) => q"$pack.CaseClass($a)"
}
implicit val identLiftable: Liftable[Ident] = Liftable[Ident] {
case Ident(a) => q"$pack.Ident($a)"
Expand Down
78 changes: 74 additions & 4 deletions quill-core/src/main/scala/io/getquill/quotation/Parsing.scala
Original file line number Diff line number Diff line change
Expand Up @@ -438,13 +438,83 @@ trait Parsing {
private def is[T](tree: Tree)(implicit t: TypeTag[T]) =
tree.tpe <:< t.tpe

private def isCaseClass[T: WeakTypeTag] = {
val symbol = c.weakTypeTag[T].tpe.typeSymbol
symbol.isClass && symbol.asClass.isCaseClass
}

private def firstConstructorParamList[T: WeakTypeTag] = {
val tpe = c.weakTypeTag[T].tpe
val paramLists = tpe.decls.collect {
case m: MethodSymbol if m.isConstructor => m.paramLists.map(_.map(_.name))
}
paramLists.toList(0)(0).map(_.toString)
}

val valueParser: Parser[Ast] = Parser[Ast] {
case q"null" => NullValue
case q"null" => NullValue
case Literal(c.universe.Constant(v)) => Constant(v)
case q"((..$v))" if (v.size > 1) => Tuple(v.map(astParser(_)))
case q"((..$v))" if (v.size > 1) => Tuple(v.map(astParser(_)))
case q"new $ccTerm(..$v)" if (isCaseClass(c.WeakTypeTag(ccTerm.tpe.erasure))) => {
val values = v.map(astParser(_))
val params = firstConstructorParamList(c.WeakTypeTag(ccTerm.tpe.erasure))
CaseClass(params.zip(values))
}
case q"(($pack.Predef.ArrowAssoc[$t1]($v1).$arrow[$t2]($v2)))" => Tuple(List(astParser(v1), astParser(v2)))
case q"io.getquill.dsl.UnlimitedTuple.apply($v)" => astParser(v)
case q"io.getquill.dsl.UnlimitedTuple.apply(..$v)" => Tuple(v.map(astParser(_)))
case q"io.getquill.dsl.UnlimitedTuple.apply($v)" => astParser(v)
case q"io.getquill.dsl.UnlimitedTuple.apply(..$v)" => Tuple(v.map(astParser(_)))
case q"$ccCompanion(..$v)" if (
ccCompanion.tpe != null &&
ccCompanion.children.length > 0 &&
isCaseClassCompanion(ccCompanion)
) => {
val values = v.map(astParser(_))
val params = firstParamList(c.WeakTypeTag(ccCompanion.tpe.erasure))
CaseClass(params.zip(values))
}
}

private def ifThenSome[T](cond: => Boolean, output: => T): Option[T] =
if (cond) Some(output) else None

private def isCaseClassCompanion(ccCompanion: Tree): Boolean = {
val output = for {
resultType <- ifThenSome(
isTypeConstructor(c.WeakTypeTag(ccCompanion.tpe.erasure)),
resultType(c.WeakTypeTag(ccCompanion.tpe.erasure))
)
firstChild <- ifThenSome(
isCaseClass(c.WeakTypeTag(resultType)) && ccCompanion.children.length > 0,
ccCompanion.children(0)
)
moduleClass <- ifThenSome(
isModuleClass(c.WeakTypeTag(firstChild.tpe.erasure)),
asClass(c.WeakTypeTag(firstChild.tpe.erasure))
)
// Fix for SI-7567 Ideally this should be
// moduleClass.companion == resultType.typeSymbol but .companion
// returns NoSymbol where in a local context (e.g. inside a method).
} yield (resultType.typeSymbol.name.toTypeName == moduleClass.name.toTypeName)
output.getOrElse(false)
}

private def isTypeConstructor[T: WeakTypeTag] =
c.weakTypeTag[T].tpe != null && c.weakTypeTag[T].tpe.typeConstructor != NoType

private def isModuleClass[T: WeakTypeTag] = {
val typeSymbol = c.weakTypeTag[T].tpe.typeSymbol
typeSymbol.isClass && typeSymbol.isModuleClass
}

private def resultType[T: WeakTypeTag] =
c.weakTypeTag[T].tpe.resultType

private def asClass[T: WeakTypeTag] =
c.weakTypeTag[T].tpe.typeSymbol.asClass

private def firstParamList[T: WeakTypeTag] = {
val tpe = c.weakTypeTag[T].tpe
tpe.paramLists(0).map(_.name.toString)
}

val actionParser: Parser[Ast] = Parser[Ast] {
Expand Down
Loading

0 comments on commit c634645

Please sign in to comment.