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

Update returning #1720

Merged
merged 4 commits into from Dec 17, 2019
Merged
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
@@ -251,7 +251,7 @@ val returnedIds = ctx.run(q) //: List[(Int, Long)]

In certain situations, we might want to return fields that are not auto generated as well. In this case we do not want
the fields to be automatically excluded from the insertion. The `returning` method is used for that.

```scala
val q = quote {
query[Product].insert(lift(Product(0, "My Product", 1011L))).returning(r => (id, description))
@@ -272,7 +272,7 @@ val q = quote {
val returnedIds = ctx.run(q) //: List[(Int, String)]
// INSERT INTO Product (description, sku) VALUES (?, ?) RETURNING id, description
```

We can also fix this situation by using an insert-meta.

```scala
@@ -285,6 +285,19 @@ val returnedIds = ctx.run(q) //: List[(Int, String)]
// INSERT INTO Product (description, sku) VALUES (?, ?) RETURNING id, description
```

##### update returning

`returning` can also be used after `update`:

```scala
val q = quote {
query[Product].update(lift(Product(42, "Updated Product", 2022L))).returning(r => (r.id, r.description))
}

val updated = ctx.run(q) //: List[(Int, String)]
// UPDATE Product SET id = ?, description = ?, sku = ? RETURNING id, description
```

#### Customization

##### Postgres
@@ -357,6 +370,16 @@ ctx.run(q.returning(r => id + 100)) //: List[Int]
// INSERT INTO Product (description, sku) OUTPUT INSERTED.id + 100 VALUES (?, ?)
```

Update returning:
```scala
val q = quote {
query[Product].update(_.description -> "Updated Product", _.sku -> 2022L).returning(r => (r.id, r.description))
}

val updated = ctx.run(q)
// UPDATE Product SET description = 'Updated Product', sku = 2022 OUTPUT INSERTED.id, INSERTED.description
```

### Embedded case classes

Quill supports nested `Embedded` case classes:
@@ -3408,4 +3431,4 @@ The project was created having Philip Wadler's talk ["A practical theory of lang

* [A Practical Theory of Language-Integrated Query](http://homepages.inf.ed.ac.uk/slindley/papers/practical-theory-of-linq.pdf)
* [Everything old is new again: Quoted Domain Specific Languages](http://homepages.inf.ed.ac.uk/wadler/papers/qdsl/qdsl.pdf)
* [The Flatter, the Better](http://db.inf.uni-tuebingen.de/staticfiles/publications/the-flatter-the-better.pdf)
* [The Flatter, the Better](http://db.inf.uni-tuebingen.de/staticfiles/publications/the-flatter-the-better.pdf)
@@ -167,8 +167,12 @@ private[getquill] trait QueryDsl {
def onConflictUpdate(target: E => Any, targets: (E => Any)*)(assign: ((E, E) => (Any, Any)), assigns: ((E, E) => (Any, Any))*): Insert[E] = NonQuotedException()
}

sealed trait Update[E] extends Action[E] {
@compileTimeOnly(NonQuotedException.message)
def returning[R](f: E => R): ActionReturning[E, R] = NonQuotedException()
}

sealed trait ActionReturning[E, Output] extends Action[E]
sealed trait Update[E] extends Action[E]
sealed trait Delete[E] extends Action[E]

sealed trait BatchAction[+A <: Action[_]]
@@ -921,7 +921,7 @@ trait Parsing extends ValueComputation {

(ident == originalBody, actionType.tpe) match {
// Note, tuples are also case classes so this also matches for tuples
case (true, ClassTypeRefMatch(cls, List(arg))) if (cls == asClass[QueryDsl#Insert[_]] && isTypeCaseClass(arg)) =>
case (true, ClassTypeRefMatch(cls, List(arg))) if (cls == asClass[QueryDsl#Insert[_]] || cls == asClass[QueryDsl#Update[_]]) && isTypeCaseClass(arg) =>

val elements = flatten(q"${TermName(ident.name)}", value("Decoder", arg))
if (elements.size == 0) c.fail("Case class in the 'returning' clause has no values")
@@ -1,11 +1,9 @@
package io.getquill.context

import io.getquill.{ Spec, testContext }
import io.getquill.ReturnAction.{ ReturnColumns, ReturnRecord }
import io.getquill.testContext._
import io.getquill.context.mirror.Row
import io.getquill.MirrorIdiomReturningSingle
import io.getquill.MirrorIdiomReturningMulti
import io.getquill.testContext._
import io.getquill.{ MirrorIdiomReturningMulti, MirrorIdiomReturningSingle, Spec, testContext }

class ActionMacroSpec extends Spec {

@@ -36,14 +34,14 @@ class ActionMacroSpec extends Spec {
}
"nexted case class lifting" in {
val q = quote {
(t: TestEntity) => qr1.insert(t)
t: TestEntity => qr1.insert(t)
}
val r = testContext.run(q(lift(TestEntity("s", 1, 2L, None))))
r.string mustEqual """querySchema("TestEntity").insert(v => v.s -> ?, v => v.i -> ?, v => v.l -> ?, v => v.o -> ?)"""
r.prepareRow mustEqual Row("s", 1, 2L, None)
}

"returning" - {
"insert returning" - {
"returning value" in {
val q = quote {
qr1.insert(t => t.i -> 1).returning(t => t.l)
@@ -54,8 +52,7 @@ class ActionMacroSpec extends Spec {
r.returningBehavior mustEqual ReturnRecord
}
"returning value - with single - should not compile" in testContext.withDialect(MirrorIdiomReturningSingle) { ctx =>
import ctx._
"ctx.run(qr1.insert(t => t.i -> 1).returning(t => t.l))" mustNot compile
"import ctx._; ctx.run(qr1.insert(t => t.i -> 1).returning(t => t.l))" mustNot compile
}
"returning value - with multi" in testContext.withDialect(MirrorIdiomReturningMulti) { ctx =>
import ctx._
@@ -158,18 +155,69 @@ class ActionMacroSpec extends Spec {
r.returningBehavior mustEqual ReturnRecord
}
}

"update returning" - {
"returning value" in {
val q = quote {
qr1.update(t => t.i -> 1).returning(t => t.l)
}
val r = testContext.run(q)
r.string mustEqual """querySchema("TestEntity").update(t => t.i -> 1).returning((t) => t.l)"""
r.prepareRow mustEqual Row()
r.returningBehavior mustEqual ReturnRecord
}
"returning value - with single - should not compile" in testContext.withDialect(MirrorIdiomReturningSingle) { ctx =>
"import ctx._; ctx.run(qr1.update(t => t.i -> 1).returning(t => t.l))" mustNot compile
}
"returning value - with multi" in testContext.withDialect(MirrorIdiomReturningMulti) { ctx =>
import ctx._
val q = quote {
qr1.update(t => t.i -> 1).returning(t => t.l)
}
val r = ctx.run(q)
r.string mustEqual """querySchema("TestEntity").update(t => t.i -> 1).returning((t) => t.l)"""
r.prepareRow mustEqual Row()
r.returningBehavior mustEqual ReturnColumns(List("l"))
}
"scalar lifting + returning value" in {
val q = quote {
qr1.update(t => t.i -> lift(1)).returning(t => t.l)
}
val r = testContext.run(q)
r.string mustEqual """querySchema("TestEntity").update(t => t.i -> ?).returning((t) => t.l)"""
r.prepareRow mustEqual Row(1)
r.returningBehavior mustEqual ReturnRecord
}
"case class lifting + returning value" in {
val q = quote {
qr1.update(lift(TestEntity("s", 1, 2L, None))).returning(t => t.l)
}
val r = testContext.run(q)
r.string mustEqual """querySchema("TestEntity").update(v => v.s -> ?, v => v.i -> ?, v => v.l -> ?, v => v.o -> ?).returning((t) => t.l)"""
r.prepareRow mustEqual Row("s", 1, 2, None)
r.returningBehavior mustEqual ReturnRecord
}
"case class lifting + returning multi value" in {
val q = quote {
qr1.update(lift(TestEntity("s", 1, 2L, None))).returning(t => (t.l, t.i))
}
val r = testContext.run(q)
r.string mustEqual """querySchema("TestEntity").update(v => v.s -> ?, v => v.i -> ?, v => v.l -> ?, v => v.o -> ?).returning((t) => (t.l, t.i))"""
r.prepareRow mustEqual Row("s", 1, 2, None)
r.returningBehavior mustEqual ReturnRecord
}
}
}

"runs batched action" - {

val entities = List(
TestEntity("s1", 2, 3L, Some(4)),
TestEntity("s5", 6, 7L, Some(8))
)

"scalar" in {
val insert = quote {
(p: Int) => qr1.insert(t => t.i -> p)
p: Int => qr1.insert(t => t.i -> p)
}
val q = quote {
liftQuery(List(1, 2)).foreach((p: Int) => insert(p))
@@ -191,7 +239,7 @@ class ActionMacroSpec extends Spec {
}
"case class + nested action" in {
val nested = quote {
(p: TestEntity) => qr1.insert(p)
p: TestEntity => qr1.insert(p)
}
val q = quote {
liftQuery(entities).foreach(p => nested(p))
@@ -230,7 +278,7 @@ class ActionMacroSpec extends Spec {
}
"scalar + returning" in {
val insert = quote {
(p: Int) => qr1.insert(t => t.i -> p).returning(t => t.l)
p: Int => qr1.insert(t => t.i -> p).returning(t => t.l)
}
val q = quote {
liftQuery(List(1, 2)).foreach((p: Int) => insert(p))
@@ -266,7 +314,7 @@ class ActionMacroSpec extends Spec {
}
"case class + returning + nested action" in {
val insert = quote {
(p: TestEntity) => qr1.insert(p).returning(t => t.l)
p: TestEntity => qr1.insert(p).returning(t => t.l)
}
val r = testContext.run(liftQuery(entities).foreach(p => insert(p)))
r.groups mustEqual List(
@@ -314,7 +362,7 @@ class ActionMacroSpec extends Spec {
}
"nested case class lifting" in {
val q = quote {
(t: TestEntity) => qr1.insert(t)
t: TestEntity => qr1.insert(t)
}
testContext.translate(q(lift(TestEntity("s", 1, 2L, None)))) mustEqual
"""querySchema("TestEntity").insert(v => v.s -> 's', v => v.i -> 1, v => v.l -> 2, v => v.o -> null)"""
@@ -358,7 +406,7 @@ class ActionMacroSpec extends Spec {

"scalar" in {
val insert = quote {
(p: Int) => qr1.insert(t => t.i -> p)
p: Int => qr1.insert(t => t.i -> p)
}
val q = quote {
liftQuery(List(1, 2)).foreach((p: Int) => insert(p))
@@ -379,7 +427,7 @@ class ActionMacroSpec extends Spec {
}
"case class + nested action" in {
val nested = quote {
(p: TestEntity) => qr1.insert(p)
p: TestEntity => qr1.insert(p)
}
val q = quote {
liftQuery(entities).foreach(p => nested(p))
@@ -415,7 +463,7 @@ class ActionMacroSpec extends Spec {
}
"scalar + returning" in {
val insert = quote {
(p: Int) => qr1.insert(t => t.i -> p).returning(t => t.l)
p: Int => qr1.insert(t => t.i -> p).returning(t => t.l)
}
val q = quote {
liftQuery(List(1, 2)).foreach((p: Int) => insert(p))
@@ -436,7 +484,7 @@ class ActionMacroSpec extends Spec {
}
"case class + returning + nested action" in {
val insert = quote {
(p: TestEntity) => qr1.insert(p).returning(t => t.l)
p: TestEntity => qr1.insert(p).returning(t => t.l)
}
testContext.translate(liftQuery(entities).foreach(p => insert(p))) mustEqual List(
"""querySchema("TestEntity").insert(v => v.s -> 's1', v => v.i -> 2, v => v.l -> 3, v => v.o -> 4).returning((t) => t.l)""",
@@ -454,7 +502,7 @@ class ActionMacroSpec extends Spec {
}
"case class + returning generated + nested action" in {
val insert = quote {
(p: TestEntity) => qr1.insert(p).returningGenerated(t => t.l)
p: TestEntity => qr1.insert(p).returningGenerated(t => t.l)
}
testContext.translate(liftQuery(entities).foreach(p => insert(p))) mustEqual List(
"""querySchema("TestEntity").insert(v => v.s -> 's1', v => v.i -> 2, v => v.o -> 4).returningGenerated((t) => t.l)""",
@@ -55,27 +55,61 @@ class JdbcContextSpec extends Spec {
}
}

"Insert with returning with single column table" in {
val inserted = testContext.run {
qr4.insert(lift(TestEntity4(0))).returning(_.i)
"insert returning" - {
"with single column table" in {
val inserted = testContext.run {
qr4.insert(lift(TestEntity4(0))).returning(_.i)
}
testContext.run(qr4.filter(_.i == lift(inserted))).head.i mustBe inserted
}

"with multiple columns" in {
testContext.run(qr1.delete)
val inserted = testContext.run {
qr1.insert(lift(TestEntity("foo", 1, 18L, Some(123)))).returning(r => (r.i, r.s, r.o))
}
(1, "foo", Some(123)) mustBe inserted
}
testContext.run(qr4.filter(_.i == lift(inserted))).head.i mustBe inserted
}

"Insert with returning with multiple columns" in {
testContext.run(qr1.delete)
val inserted = testContext.run {
qr1.insert(lift(TestEntity("foo", 1, 18L, Some(123)))).returning(r => (r.i, r.s, r.o))
"with multiple columns - case class" in {
case class Return(id: Int, str: String, opt: Option[Int])
testContext.run(qr1.delete)
val inserted = testContext.run {
qr1.insert(lift(TestEntity("foo", 1, 18L, Some(123)))).returning(r => Return(r.i, r.s, r.o))
}
Return(1, "foo", Some(123)) mustBe inserted
}
(1, "foo", Some(123)) mustBe inserted
}

"Insert with returning with multiple columns - case class" in {
case class Return(id: Int, str: String, opt: Option[Int])
testContext.run(qr1.delete)
val inserted = testContext.run {
qr1.insert(lift(TestEntity("foo", 1, 18L, Some(123)))).returning(r => Return(r.i, r.s, r.o))
"update returning" - {
"with single column table" in {
testContext.run(qr4.insert(lift(TestEntity4(8))))

val updated = testContext.run {
qr4.update(lift(TestEntity4(0))).returning(_.i)
}
testContext.run(qr4.filter(_.i == lift(updated))).head.i mustBe updated
}

"with multiple columns" in {
testContext.run(qr1.delete)
testContext.run(qr1.insert(lift(TestEntity("baz", 6, 42L, Some(456)))))

val updated = testContext.run {
qr1.update(lift(TestEntity("foo", 1, 18L, Some(123)))).returning(r => (r.i, r.s, r.o))
}
(1, "foo", Some(123)) mustBe updated
}

"with multiple columns - case class" in {
case class Return(id: Int, str: String, opt: Option[Int])
testContext.run(qr1.delete)
testContext.run(qr1.insert(lift(TestEntity("baz", 6, 42L, Some(456)))))

val updated = testContext.run {
qr1.update(lift(TestEntity("foo", 1, 18L, Some(123)))).returning(r => Return(r.i, r.s, r.o))
}
Return(1, "foo", Some(123)) mustBe updated
}
Return(1, "foo", Some(123)) mustBe inserted
}
}