-
Notifications
You must be signed in to change notification settings - Fork 348
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
[WIP] inserted record can be returned from query #1383
Conversation
MySQL doesn't support The question now is if we should restrict that feature to postgres only and how to implement that? EDIT: At MySQL |
Thanks for the awesome work @rolandjohann. Most databases actualy don't support record returns at all. I think Postgres is the exception rather then the norm, so we have to add this case to each DB test. Since What I suggest is that you add a Also, instead of having a separate |
@deusaquilus great idea defining capabilities, I'll try to implement that. Regarding the API I'm not sure either how to define it. In the first version the signature required the type explicitly case class Person(id: Long, name: String, age: int, createdAt: DateTime)
val personToCreate = Person(
id = 0L,
name = "Bob", // ignored by query
age = 123,
createdAt = new DateTime() // ignored by query
)
query[Person].insert(lift(personToCreate)) My current concept involves prototypes of entities, so I can share/unify domain models even outside the context of DAO/Repository and can use them at other layers (HTTP via REST for example): case class Person(id: Long, name: String, age: int, createdAt: DateTime)
case class PersonPrototype(name: String, age: int) With this DB facing domain model we can implement insert query by prototype and specify return type explicitly to complete entity: class PersonRepository {
def create(personPrototype: PersonPrototype): Future[Person] = {
implicit val m = schemaMeta[PersonPrototype]("person")
query[PersonPrototype].insert(lift(personPrototype)).returningRecord[Person]
}
} Currently I see two possibilities:
def returning[R](f: E => R): ActionReturning[E, R]
def returning[R]: ActionReturning[E, R] This offers flexibility, but adds verbosity because we must specify return type explicitly even if it is the same of What are your thoughts on this? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rolandjohann Actually, I really like the idea of forcing the user to specify the return type. I think just having insert(foo).returning
is very unclear in the first place. What is being returned? Is it the Id? Is the number of records inserted? There's no good reason to assume one over another. Admittedly, returningRecord
clears up this confusion a little bit but the meaning of "record" isn't entirely transparent either. Let's go with the "overloading" option.
@@ -698,6 +698,8 @@ trait Parsing { | |||
Delete(astParser(query)) | |||
case q"$action.returning[$r](($alias) => $body)" => | |||
Returning(astParser(action), identParser(alias), astParser(body)) | |||
case q"$action.returningRecord" => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about something like this (or some simplified variation of this)?
case q"$action.returning[$r](($alias) => $body)" =>
val tpe =
body match {
case Ident(TermName(n)) if (n == alias.name.toString) => Some(x.tpe)
case _ => None
}
val argTpe =
alias match {
case q"$mods val $pat: $tpt = $expr" =>
Some(tpt.tpe)
case _ =>
None
}
(tpe, argTpe) match {
case (Some(a), Some(b)) if (a =:= b) =>
ReturningRecord(astParser(action))
case _ =>
Returning(astParser(action), identParser(alias), astParser(body))
}
}
I'd rather not introduce additional constructs to the DSL if it's technically not needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you mean with the "overloading" option? If I understand the suggested snipped correctly, it checks if the lambda passed to returning
is of type T => T
. With that API users aren't able specify the return type.
What about introducing .returningAs[R]
and don't touch parsing of . returning
at all? Introducing an additional API seems to be less complex at implementation and more flexible for the user.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rolandjohann So I think .returning(e => e)
should work, otherwise we'll get WTFs from users. It's an expectations thing. It's totally reasonable for a user to ask "So wait, why is this API for whole records, and this one for single fields? That's a totally arbitrary distinction."
Also, are you saying that overloading this method now forces all calls to .returning
then to specify the generic parameter including the regular use-case .returning(a -> a.id)
? That definitely calls for introducing returningAs
! Is this the case?
Why can't we do something like this (see the bottom for the addition)?
case q"$action.returning[$r](($alias) => $body)" =>
val tpe =
body match {
case Ident(TermName(n)) if (n == alias.name.toString) => Some(x.tpe)
case _ => None
}
val argTpe =
alias match {
case q"$mods val $pat: $tpt = $expr" =>
Some(tpt.tpe)
case _ =>
None
}
(tpe, argTpe) match {
case (Some(a), Some(b)) if (a =:= b) =>
ReturningRecord(astParser(action))
case _ =>
Returning(astParser(action), identParser(alias), astParser(body))
}
}
// Option A (if returning can be overloaded without verbosity increase)
//case q"$action.returning[$r](...$paramss)" if (paramss.length == 0) =>
// Option B (if returningAs needs to be introduced)
case q"$action.returningAs[$r](...$paramss)" =>
ReturningRecord(astParser(action))
If there's something I missed here or if this is not as simple as I think, let me know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, just recognized that I missed a point you mentioned earlier.
We can go with overloading, for this we must introduce the additional signature at QueryDsl
. Just using the old one and omit the lambda which normally defines the column to return will return a function type:
val q1: (Person => Person) => ctx.ActionReturning[Person, Person] = query[Person].insert(lift(Person(0L, "Roland", 29))).returning[Person]
val q2: ctx.ActionReturning[Person, Long] = query[Person].insert(lift(Person(0L, "Roland", 29))).returning(_.id)
scala> quote(query[Person].insert(lift(Person(0L, "Roland", 29))).returning[Person])
<console>:18: error: missing argument list for method returning in trait Insert
Unapplied methods are only converted to functions when a function type is expected.
You can make this conversion explicit by writing `returning _` or `returning(_)` instead of `returning`.
quote(query[Person].insert(lift(Person(0L, "Roland", 29))).returning[Person])
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. That's what I was thinking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And we must define the type when calling to get Entity, too.
.returning[Person]
.returning(_.id)
@@ -205,6 +205,7 @@ class MirrorIdiom extends Idiom { | |||
case Insert(query, assignments) => stmt"${query.token}.insert(${assignments.token})" | |||
case Delete(query) => stmt"${query.token}.delete" | |||
case Returning(query, alias, body) => stmt"${query.token}.returning((${alias.token}) => ${body.token})" | |||
case ReturningRecord(query) => stmt"${query.token}.returningRecord" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need to transport the type here as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes
I tried a naive approach: My first thought was to define the additional method only for postgres contexts with an implicit class, so users can't use this feature on non postrges contexts and get errors at compile time. But the PoC implementation crashed at compilation:
As I'm not familiar with macros, I can't guess if that approach even can work. |
Yeah, defining this kind of functionality outside of the macro parser is very difficult. |
@deusaquilus Need your help/assistance again. The DB contexts aren't directly within the same class hierarchy as Things I tried to get
class ParserMacroSettings(insertReturn: InsertReturnCapability) extends StaticAnnotation
trait Capabilities {
def insertReturn: InsertReturnCapability
}
trait Context[Idiom <: io.getquill.idiom.Idiom, Naming <: NamingStrategy] extends Capabilities {
// for testing purposes to prevent having to implement that at all classes extending Context
override def insertReturn: InsertReturnCapability = ReturnSingleField
@ParserMacroSettings(insertReturn) def run[T](quoted: Quoted[ActionReturning[_, T]]): Result[RunActionReturningResult[T]] = macro ActionMacro.runActionReturning[T]
} Now at macro I have the AST of annotation body with c.macroApplication
.children
.head
.symbol
.annotations
.find(_.tree.tpe <:< typeOf[ParserMacroSettings])
.map(annotation => showRaw(annotation.tree.children.tail.head)) which obviously is just
So basically I'm stuck here |
@deusaquilus I suggest finalizing this particular feature and do the restriction to postgres afterwards. In that step mysql capability can be handled, too: IMHO there the API should restrict to returning ID and even provide an API that's pointing to that fact |
Hi @rolandjohann. I’m celebrating the Jewish holiday of Purim this week so
going to be occupied most of the time. I’ll try to have another look over
the weekend and address any issues.
…On Wed, Mar 20, 2019 at 6:34 AM rolandjohann ***@***.***> wrote:
@deusaquilus <https://github.com/deusaquilus> I suggest finalizing this
particular feature and do the restriction to postgres afterwards. In that
step mysql capability can be handled, too: IMHO there the API should
restrict to returning ID and even provide an API that's pointing to that
fact returningId.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#1383 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/ABTliBtLlnJEgfx-uXIZecewDPFPppeiks5vYg6ZgaJpZM4bsufa>
.
|
Hi @deusaquilus thanks for the information |
Sorry but I can't do this tonight. Banged my head on the codegen commit (#1396) and need to collapse now. Will have a look at this the coming day/evening. |
Okay. The capabilities have to be read during compile-time as opposed to run-time so you cannot encode them into a value, you need to encode them into a type. You can then read that back out using sealed trait InsertReturnCapability
trait ReturnSingleField extends InsertReturnCapability
trait ReturnMultipleField extends InsertReturnCapability
case object ReturnSingleField extends ReturnSingleField
case object ReturnMultipleField extends ReturnMultipleField
trait Context[Idiom <: io.getquill.idiom.Idiom, Naming <: NamingStrategy] {
type InsertReturnCapabilityType <: InsertReturnCapability
override def insertReturnCapability:InsertReturnCapabilityType
}
// Then in a paticular sub-context you do:
trait PostgresJdbcContext[Idiom <: io.getquill.idiom.Idiom, Naming <: NamingStrategy] {
override type InsertReturnCapabilityType = ReturnMultipleField
override def insertReturnCapability: ReturnMultipleField = ReturnMultipleField
} Then in your macro code do this to check the actual return type that the context has. val capabilityType =
c.prefix.tree.tpe.members
.filter(_.isMethod)
.filter(_.name == TermName("myColor"))
.head
.asMethod
.returnType
// check it like this:
if (capabilityType =:= ReturnMultipleField)
doOneThing()
else
doAnotherThing() In the above code I didn't abstract out into a Capabilities trait but you should definitely try to do that: trait ReturnMultipleCapability {
override type InsertReturnCapabilityType = ReturnSingleField
override def insertReturnCapability:ReturnSingleField
}
// and the same for ReturnSingleCapability
// Then you tack them both onto Context This compile-time stuff is a little mind-bending sometimes. Thanks for the good work! |
Thanks for sharing. I'll try that within the next few days |
@rolandjohann Any luck? I’m going to be away on vacation until next week so my connectivity will be limited |
Yes, had some time today. I‘ll push the changes this evening.
Alexander Ioffe <notifications@github.com> schrieb am So. 7. Apr. 2019 um
16:26:
… @ronaldjohann Any luck? I’m going to be away on vacation until next week
so my connectivity will be limited
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#1383 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AFddGgaMttlgGiYAy2__7dfFsvQUQ4A1ks5vegALgaJpZM4bsufa>
.
|
@deusaquilus got it working, but I'm not happy with the current situation: Have a nice time on vacation! |
As far as I can see we can use Idiom to check for returning capability - that seems to be far more complicated to implement |
Shoot! You're right. I know this should be possible but I don't want to you wait any longer. Please PR the change without the capabilities-checking and file an issue that we need a capabilities-checking feature (in the future) in the |
Okay. Here's how you do it from sealed trait InsertReturnCapability
trait ReturnSingleField extends InsertReturnCapability
trait ReturnMultipleField extends InsertReturnCapability
trait Capabilities {
type ReturnAfterInsert <: InsertReturnCapability
}
trait CanReturnRecordAfterInsert extends Capabilities {
override type ReturnAfterInsert = ReturnMultipleField
}
trait PostgresDialect
extends SqlIdiom
with QuestionMarkBindVariables
with ConcatSupport
with CanReturnRecordAfterInsert /* Add this last part here */ {
...
} Here's how you then check it in the macro: // c.prefix.tree.tpe.typeArgs(0) should be the Dialect e.g. PostgresDialect
c.prefix.tree.tpe.typeArgs(0).members.find {
case ts:TypeSymbol if (ts.asType.typeSignature =:= typeOf[ReturnMultipleField]) => true
case _ => false
} I haven't tried it outside of my mock example yet but going to do it. Want to give this a shot? |
@deusaquilus thanks for sharing the solution, I'll try that (hopefully) at the weekend. Time's rare currently |
…iple fields with returning clause
…will bail out with idiom fqcn contained at error message
@rolandjohann How is it going? Did this approach work? |
@deusaquilus sorry for the delay. Yes, your approach work perfectly. But having a quick look at the CI tests depicts plenty of failing tests because of non provided type when returning a single field. I tested the stuff as well by setting up a clean project with snapshot dependency on local published quill libs and there I have no problems at all. Even IntelliJ is very restrictive in hinting scala code and doesn't criticise about non provided types. Can it be that this is caused by some specific compiler flag? BTW: If I remember correctly you asked for type specification when overloading... |
@rolandjohann It looks like we need to do |
@deusaquilus as we tied the capability to There I didn't face the issues that let the tests crash. Do you know what's the root cause of this? |
@rolandjohann Are you sure there's nothing missing here? I just tried the following with val prd = Product(0L, "test1", 1L)
val insertedProduct = testContext.run {
product.insert(_.sku -> lift(prd.sku), _.description -> lift(prd.description)).returning[Product]
}
val returnedProduct = testContext.run(productById(lift(insertedProduct.id))).head
returnedProduct mustEqual insertedProduct ... and I got:
This does not produce a query that has a Information:(43, 45) INSERT INTO Product (sku,description) VALUES (?, ?)
val insertedProduct = testContext.run { |
@rolandjohann I got this to compile the tests to work but it doesn't work with |
Okay. I managed to compile and test this change but I cannot release it because there are a couple of problems and open questions. @rolandjohann Taking this functionality and bolting-on the Query decoder a brilliant 1st step but there a couple of other things that have to be done before the feature is complete. new Product(
implicitly[Decoder[Long]](longDecoder).apply(0, row),
implicitly[Decoder[String]](stringDecoder).apply(1, row),
implicitly[Decoder[Long]](longDecoder).apply(2, row))
) Yet because the In order to resolve this issue, we need to answer an essential question. The
@rolandjohann @fwbrasil and @getquill/maintainers. I'd love to hear your thoughts on how this should be done. |
It's interesting to note that actually, the reason why my query fails above is that the columns arrive in the result-set in the wrong order and break in the decoders. If we were to do decoding based on column name as opposed to position, this problem would not happen in the first place and this implementation would actually work! |
I have a possible solution #3 which I like the most: |
Scratch that. Here's what I think it needs to look like: object ExpandReturning {
def apply(returning: Returning, idiom: SqlIdiom)(implicit naming: NamingStrategy): List[String] = {
val Returning(_, alias, property) = returning
// Ident("j"), Tuple(List(Property(Ident("j"), "name"), BinaryOperation(Property(Ident("j"), "age"), +, Constant(1))))
// => Tuple(List(Constant("name"), BinaryOperation(Ident("age"), +, Constant(1))))
val dePropertized =
Transform(property) {
case Property(`alias`, rest) => Ident(rest)
}
// Tuple(List(Constant("name"), BinaryOperation(Ident("age"), +, Constant(1))))
// => List(Constant("name"), BinaryOperation(Ident("age"), +, Constant(1)))
val deTuplified = dePropertized match {
case Tuple(values) => values
case CaseClass(values) => values.map(_._2)
case other => List(other)
}
deTuplified
.map(field => idiom.defaultTokenizer.token(field))
.map(_.toString)
}
} |
I'm continuing this work in #1489. Bunch of stuff left to do but progress is being made. |
@deusaquilus sorry for the disruption on this implementation. Unfortunately I have no time to contribute to this feature as I would like to. My experiments and implementation used With your suggested solution, will it still be possible to have an API like |
|
Fixes #572
Problem
When inserting records only one column can be returned via
RETURNING
clause. There are several use cases of database generated values besides IDs, for example timestamps.Solution
Added
returningRecord
toQueryDsl
including all relevant macro implementations.Notes
The current implementation works with PostgreSQL, not tested with other DBs. This is my first contact with Macros, so please review carefully.
Unit tests and README update will be done in the next few days.
Checklist
README.md
if applicable[WIP]
to the pull request title if it's work in progresssbt scalariformFormat test:scalariformFormat
to make sure that the source files are formatted@getquill/maintainers