Skip to content

Commit

Permalink
Fixing case of filter(lift).insert/updateValue (#76)
Browse files Browse the repository at this point in the history
  • Loading branch information
deusaquilus authored Feb 14, 2022
1 parent 26d4ab8 commit 8f01f32
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,51 @@ class InsertAdvancedSpec extends Spec with Inside {
ctx.run(q).triple mustEqual ("UPDATE tblPerson SET colName = 'Joe'", List(), Static)
ctx.run(a).triple mustEqual ("UPDATE tblPerson SET colName = 'Joe', colAge = 123", List(), Static)
}

"simple with schemaMeta with extra columns and update meta fully lifted" in {
inline def q = quote { query[Person].filter(e => e.name == "JoeJoe").updateValue(lift(Person("Joe", 123))) }
inline def a = quote { query[Person].filter(e => e.name == "JoeJoe").update(_.name -> "Joe", _.age -> 123) }
inline given personSchema: UpdateMeta[Person] = updateMeta[Person](_.age)
inline given sm: SchemaMeta[Person] = schemaMeta("tblPerson", _.name -> "colName", _.age -> "colAge")
ctx.run(q).triple mustEqual ("UPDATE tblPerson SET colName = ? WHERE colName = 'JoeJoe'", List("Joe"), Static)
ctx.run(a).triple mustEqual ("UPDATE tblPerson SET colName = 'Joe', colAge = 123 WHERE colName = 'JoeJoe'", List(), Static)
}

"simple with schemaMeta with extra columns and update meta fully lifted with filter lift" in {
inline def q = quote { query[Person].filter(e => e.name == lift("JoeJoe")).updateValue(lift(Person("Joe", 123))) }
inline def a = quote { query[Person].filter(e => e.name == lift("JoeJoe")).update(_.name -> "Joe", _.age -> 123) }
inline given personSchema: UpdateMeta[Person] = updateMeta[Person](_.age)
inline given sm: SchemaMeta[Person] = schemaMeta("tblPerson", _.name -> "colName", _.age -> "colAge")
ctx.run(q).triple mustEqual ("UPDATE tblPerson SET colName = ? WHERE colName = ?", List("Joe", "JoeJoe"), Static)
ctx.run(a).triple mustEqual ("UPDATE tblPerson SET colName = 'Joe', colAge = 123 WHERE colName = ?", List("JoeJoe"), Static)
}

"simple with schemaMeta with extra columns and update meta filter lift - included column" in {
inline def q = quote { query[Person].filter(e => e.name == lift("JoeJoe")).updateValue(Person(lift("Joe"), 123)) }
inline def a = quote { query[Person].filter(e => e.name == lift("JoeJoe")).update(_.name -> lift("Joe"), _.age -> 123) }
inline given personSchema: UpdateMeta[Person] = updateMeta[Person](_.age)
inline given sm: SchemaMeta[Person] = schemaMeta("tblPerson", _.name -> "colName", _.age -> "colAge")
ctx.run(q).triple mustEqual ("UPDATE tblPerson SET colName = ? WHERE colName = ?", List("Joe", "JoeJoe"), Static)
ctx.run(a).triple mustEqual ("UPDATE tblPerson SET colName = ?, colAge = 123 WHERE colName = ?", List("Joe", "JoeJoe"), Static)
}

"simple with schemaMeta with extra columns and update meta filter lift - excluded column" in {
inline def q = quote { query[Person].filter(e => e.name == lift("JoeJoe")).updateValue(Person("Joe", lift(123))) }
inline def a = quote { query[Person].filter(e => e.name == lift("JoeJoe")).update(_.name -> "Joe", _.age -> lift(123)) }
inline given personSchema: UpdateMeta[Person] = updateMeta[Person](_.age)
inline given sm: SchemaMeta[Person] = schemaMeta("tblPerson", _.name -> "colName", _.age -> "colAge")
ctx.run(q).triple mustEqual ("UPDATE tblPerson SET colName = 'Joe' WHERE colName = ?", List("JoeJoe"), Static)
ctx.run(a).triple mustEqual ("UPDATE tblPerson SET colName = 'Joe', colAge = ? WHERE colName = ?", List(123, "JoeJoe"), Static)
}

"simple with schemaMeta with extra columns and update meta filter lift - filter by excluded column" in {
inline def q = quote { query[Person].filter(e => e.age == lift(123)).updateValue(Person("Joe", lift(123))) }
inline def a = quote { query[Person].filter(e => e.age == lift(123)).update(_.name -> "Joe", _.age -> lift(123)) }
inline given personSchema: UpdateMeta[Person] = updateMeta[Person](_.age)
inline given sm: SchemaMeta[Person] = schemaMeta("tblPerson", _.name -> "colName", _.age -> "colAge")
ctx.run(q).triple mustEqual ("UPDATE tblPerson SET colName = 'Joe' WHERE colAge = ?", List(123), Static)
ctx.run(a).triple mustEqual ("UPDATE tblPerson SET colName = 'Joe', colAge = ? WHERE colAge = ?", List(123, 123), Static)
}
}

"simple - runtime" in {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import io.getquill.context.mirror.Row
import io.getquill.context.sql.testContext
import io.getquill.context.sql.testContext._
import io.getquill._
import io.getquill.context.ExecutionType.Static
import io.getquill.context.ExecutionType.Dynamic

class ActionSpec extends Spec {
"action" - {
Expand Down Expand Up @@ -124,14 +126,103 @@ class ActionSpec extends Spec {
"INSERT INTO TestEntity4 DEFAULT VALUES"
}
}
"update" - {
"inline updateValue " - {
"entity" in {
inline def q = quote {
qr1.filter(t => t.s == "s").updateValue(TestEntity("s", 1, 2L, Some(1), true))
}
testContext.run(q).string mustEqual
"UPDATE TestEntity SET s = 's', i = 1, l = 2, o = 1, b = true WHERE s = 's'"
}
"entity with filter" in {
inline def q = quote {
qr1.filter(t => t.s == "s").updateValue(TestEntity("s", 1, 2L, Some(1), true))
}
testContext.run(q).string mustEqual
"UPDATE TestEntity SET s = 's', i = 1, l = 2, o = 1, b = true WHERE s = 's'"
}
"entity with filter and lift" in {
inline def q = quote {
qr1.filter(t => t.s == lift("s")).updateValue(TestEntity("s", 1, 2L, Some(1), true))
}
testContext.run(q).triple mustEqual
("UPDATE TestEntity SET s = 's', i = 1, l = 2, o = 1, b = true WHERE s = ?", List("s"), Static)
}
}
"updateValue" - {
val v = TestEntity("s", 1, 2L, Some(1), true)
"with filter" in {
inline def q = quote {
qr1.filter(t => t.s == "s").updateValue(lift(v))
}
val result = testContext.run(q)
result.triple mustEqual
("UPDATE TestEntity SET s = ?, i = ?, l = ?, o = ?, b = ? WHERE s = 's'", List("s", 1, 2L, Some(("_1", 1)), true), Static)
}
"with filter and lift" in {
inline def q = quote {
qr1.filter(t => t.s == lift("s")).updateValue(lift(v))
}
val result = testContext.run(q)
result.triple mustEqual
("UPDATE TestEntity SET s = ?, i = ?, l = ?, o = ?, b = ? WHERE s = ?", List("s", 1, 2L, Some(("_1", 1)), true, "s"), Static)
}
"quoted with filter and lift" in {
inline def orig = quote {
qr1.filter(t => t.s == lift("s"))
}
inline def q = quote {
orig.updateValue(lift(v))
}
val result = testContext.run(q)
result.triple mustEqual
("UPDATE TestEntity SET s = ?, i = ?, l = ?, o = ?, b = ? WHERE s = ?", List("s", 1, 2L, Some(("_1", 1)), true, "s"), Static)
}
"quoted dynamic with filter and lift" in {
val orig = quote {
qr1.filter(t => t.s == lift("s"))
}
inline def q = quote {
orig.updateValue(lift(v))
}
val result = testContext.run(q)
result.triple mustEqual
("UPDATE TestEntity SET s = ?, i = ?, l = ?, o = ?, b = ? WHERE s = ?", List("s", 1, 2L, Some(("_1", 1)), true, "s"), Dynamic)
}
"fully dynamic with filter and lift" in {
val orig = quote {
qr1.filter(t => t.s == lift("s"))
}
val q = quote {
orig.updateValue(lift(v))
}
testContext.run(q).triple mustEqual
("UPDATE TestEntity SET s = ?, i = ?, l = ?, o = ?, b = ? WHERE s = ?", List("s", 1, 2L, Some(("_1", 1)), true, "s"), Dynamic)
}
}
"update" - {
"with filter - null" in {
inline def q = quote {
qr1.filter(t => t.s == null).update(_.s -> "s")
}
testContext.run(q).string mustEqual
"UPDATE TestEntity SET s = 's' WHERE s IS NULL"
}
"with filter" in {
inline def q = quote {
qr1.filter(t => t.s == "s").update(_.s -> "s")
}
testContext.run(q).string mustEqual
"UPDATE TestEntity SET s = 's' WHERE s = 's'"
}
"with filter and lift" in {
inline def q = quote {
qr1.filter(t => t.s == lift("s")).update(_.s -> "s")
}
val result = testContext.run(q)
result.triple mustEqual
("UPDATE TestEntity SET s = 's' WHERE s = ?", List("s"), Static)
}
"without filter" in {
val q = quote {
qr1.update(_.s -> "s")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,15 @@ object InsertUpdateMacro {
case other => throw new IllegalArgumentException(s"Invalid values in InsertMeta: ${other}. An InsertMeta AST must be a tuple of Property elements.")
}

enum SummonState[+T]:
case Static(value: T) extends SummonState[T]
case Dynamic(uid: String, quotation: Expr[Quoted[Any]]) extends SummonState[Nothing]
// Summon state of a schemaMeta (i.e. whether an implicit one could be summoned and whether it is static (i.e. can produce a compile-time query or dynamic))
enum EntitySummonState[+T]:
case Static(value: T, lifts: List[Expr[Planter[?, ?, ?]]]) extends EntitySummonState[T]
case Dynamic(uid: String, quotation: Expr[Quoted[Any]]) extends EntitySummonState[Nothing]

// Summon state of a updateMeta/insertMeta that indicates which columns to ignore (i.e. whether an implicit one could be summoned and whether it is static (i.e. can produce a compile-time query or dynamic))
enum IgnoresSummonState[+T]:
case Static(value: T) extends IgnoresSummonState[T]
case Dynamic(uid: String, quotation: Expr[Quoted[Any]]) extends IgnoresSummonState[Nothing]

/**
* Perform the pipeline of creating an insert statement. The 'insertee' is the case class on which the SQL insert
Expand All @@ -116,12 +122,13 @@ object InsertUpdateMacro {
val entityName = TypeRepr.of[T].classSymbol.get.name
Entity(entityName, List(), InferQuat.of[T].probit)

def summon: SummonState[Ast] =
def summon: EntitySummonState[Ast] =
val schema = schemaRaw.asTerm.underlyingArgument.asExprOf[EntityQuery[T]]
UntypeExpr(schema) match
// Case 1: query[Person].insert(...)
// the schemaRaw part is {query[Person]} which is a plain entity query (as returned from QueryMacro)
case '{ EntityQuery[t] } => SummonState.Static(plainEntity)
case '{ EntityQuery[t] } =>
EntitySummonState.Static(plainEntity, Nil)
// Case 2: querySchema[Person](...).insert(...)
// there are query schemas involved i.e. the {querySchema[Person]} part is a QuotationLotExpr.Unquoted that has been spliced in
// also if there is an implicit/given schemaMeta this case will be hit (because if there is a schemaMeta,
Expand All @@ -130,21 +137,30 @@ object InsertUpdateMacro {
case QuotationLotExpr.Unquoted(unquotation) =>
unquotation match
// The {querySchema[Person]} part is static (i.e. fully known at compile-time)
case Uprootable.Ast(ast) =>
// (also note that if it's a filter with a pre-existing lift unlift(query[Person]).filter(p => lift("Joe")).insertValue(...)
// this case will also happen and there can be one or more lifts i.e. lift("Joe") coming from the filter clause)
case Uprootable(_, ast, lifts) =>
val unliftedAst = Unlifter(ast)
// This should be some tree containing an entity (it could even be an infix containing an entity).
// Maybe should actually check there's an entity inside in the future.
SummonState.Static(unliftedAst)
// (note that we want to replant the lifts because they do not need to be extracted here, just put back into the resulting quotation of the insert/updateValue method below)
EntitySummonState.Static(unliftedAst, lifts.map(_.plant))
// The {querySchema[Person]} is dynamic (i.e. not fully known at compile-time)
case Pluckable(uid, quotation, _) =>
SummonState.Dynamic(uid, quotation)
EntitySummonState.Dynamic(uid, quotation)
case _ =>
report.throwError(s"Quotation Lot of InsertMeta either pluckable or uprootable from: '${unquotation}'")
report.throwError(s"Quotation Lot of Insert/UpdateMeta must be either pluckable or uprootable from: '${unquotation}'")

// parse this
// Case where it's not just an EntityQuery that is in the front of the update/insertValue e.g. query[Person].filter(...).update/insertValue
// (also note that if it's a filter with a pre-existing lift query[Person].filter(p => lift("Joe")).insertValue(...)
// this case will also happen and there can be one or more lifts i.e. lift("Joe") coming from the filter clause.
// that is why we need to extract the lifts)
// Note that there is no unquotation in this case so there should be no possibility of having runtimeUnquotes here
case '{ ($q: EntityQuery[t]) } =>
val ast = parser(q)
SummonState.Static(ast)
val (rawLifts, runtimeLifts) = ExtractLifts(q)
if (!runtimeLifts.isEmpty)
report.throwError(s"Runtime lifts encountered in a fully spliced entity passed to .insert/updateValue:\n${runtimeLifts.map(Format.Expr(_)).mkString(",\n")}.\nThis is Illegal")
EntitySummonState.Static(ast, rawLifts)

case _ =>
report.throwError(s"Cannot process illegal insert meta: ${Format.Expr(schema)}")
Expand Down Expand Up @@ -172,7 +188,7 @@ object InsertUpdateMacro {


object IgnoredColumns:
def summon: SummonState[Set[Ast]] =
def summon: IgnoresSummonState[Set[Ast]] =
// If someone has defined a: given meta: InsertMeta[Person] = insertMeta[Person](_.id) or UpdateMeta[Person] = updateMeta[Person](_.id)
MacroType.summonMetaOfThis() match
case Some(actionMeta) =>
Expand All @@ -181,18 +197,18 @@ object InsertUpdateMacro {
case Uprootable.Ast(ast) =>
Unlifter(ast) match
case Tuple(values) if (values.forall(_.isInstanceOf[Property])) =>
SummonState.Static(values.toSet)
IgnoresSummonState.Static(values.toSet)
case other =>
report.throwError(s"Invalid values in ${Format.TypeRepr(actionMeta.asTerm.tpe)}: ${other}. An ${Format.TypeRepr(actionMeta.asTerm.tpe)} AST must be a tuple of Property elements.")
// if the meta is not inline
case Pluckable(uid, quotation, _) =>
SummonState.Dynamic(uid, quotation)
IgnoresSummonState.Dynamic(uid, quotation)
case _ =>
report.throwError(s"The ${MacroType.asString}Meta form is invalid. It is Pointable: ${io.getquill.util.Format.Expr(actionMeta)}. It must be either Uprootable or Pluckable i.e. it has at least a UID that can be identified.")
// TODO Configuration to ignore dynamic insert metas?
//println("WARNING: Only inline insert-metas are supported for insertions so far. Falling back to a insertion of all fields.")
case None =>
SummonState.Static(Set.empty)
IgnoresSummonState.Static(Set.empty)

/**
* Inserted object
Expand Down Expand Up @@ -303,13 +319,13 @@ object InsertUpdateMacro {
def processAssignmentsAndExclusions(assignmentsOfEntity: List[io.getquill.ast.Assignment]): AssignmentList =
IgnoredColumns.summon match
// If we have assignment-exclusions during compile time
case SummonState.Static(exclusions) =>
case IgnoresSummonState.Static(exclusions) =>
// process which assignments to exclude and take them out
val remainingAssignments = assignmentsOfEntity.filterNot(asi => exclusions.contains(asi.property))
// Then just return the remaining assignments
AssignmentList.Static(remainingAssignments)
// If we have assignment-exclusions that can only be accessed during runtime
case SummonState.Dynamic(uid, quotation) =>
case IgnoresSummonState.Dynamic(uid, quotation) =>
// Pull out the exclusions from the quotation
val exclusions = '{ DynamicUtil.retrieveAssignmentTuple($quotation) }
// Lift ALL the assignments of the entity
Expand Down Expand Up @@ -357,7 +373,7 @@ object InsertUpdateMacro {
* Create a static or dynamic quotation based on the state. Wrap the expr using some additional functions if we need to.
* This is used for the createFromPremade if we need to wrap it into insertReturning which is used for batch-returning query execution.
*/
def createQuotation(summonState: SummonState[Ast], assignmentOfEntity: List[Assignment], lifts: List[Expr[Planter[?, ?, ?]]], pluckedUnquotes: List[Expr[QuotationVase]]) = {
def createQuotation(summonState: EntitySummonState[Ast], assignmentOfEntity: List[Assignment], lifts: List[Expr[Planter[?, ?, ?]]], pluckedUnquotes: List[Expr[QuotationVase]]) = {
//println("******************* TOP OF APPLY **************")
// Processed Assignments AST plus any lifts that may have come from the assignments AST themsevles.
// That is usually the case when
Expand All @@ -366,7 +382,7 @@ object InsertUpdateMacro {
// TODO where if there is a schemaMeta? Need to use that to create the entity
(summonState, assignmentList) match
// If we can get a static entity back
case (SummonState.Static(entity), AssignmentList.Static(assignmentsAst)) =>
case (EntitySummonState.Static(entity, previousLifts), AssignmentList.Static(assignmentsAst)) =>
// Lift it into an `Insert` ast, put that into a `quotation`, then return that `quotation.unquote` i.e. ready to splice into the quotation from which this `.insert` macro has been called
val action = MacroType.ofThis() match
case MacroType.Insert =>
Expand All @@ -375,13 +391,13 @@ object InsertUpdateMacro {
AUpdate(entity, assignmentsAst)

// Now create the quote and lift the action. This is more efficient then the alternative because the whole action AST can be serialized
val quotation = '{ Quoted[A[T]](${Lifter(action)}, ${Expr.ofList(lifts)}, ${Expr.ofList(pluckedUnquotes)}) }
val quotation = '{ Quoted[A[T]](${Lifter(action)}, ${Expr.ofList(previousLifts ++ lifts)}, ${Expr.ofList(pluckedUnquotes)}) }
// Unquote the quotation and return
quotation

// If we get a dynamic entity back we need to splice things as an Expr even if the assignmentsList is know at compile time
// e.g. entityQuotation is 'querySchema[Person](...)' which is not inline
case (SummonState.Dynamic(uid, entityQuotation), assignmentsList) =>
case (EntitySummonState.Dynamic(uid, entityQuotation), assignmentsList) =>
// Need to create a ScalarTag representing a splicing of the entity (then going to add the actual thing into a QuotationVase and add to the pluckedUnquotes)
val action = MacroType.ofThis() match
case MacroType.Insert =>
Expand All @@ -397,6 +413,8 @@ object InsertUpdateMacro {
// Unquote the quotation and return
quotation

// TODO Need a catch-all

// use the quoation macro to parse the value into a class expression
// use that with (v) => (v) -> (class-value) to create a quoation
// incorporate that into a new quotation, use the generated quotation's lifts and runtime lifts
Expand Down
Loading

0 comments on commit 8f01f32

Please sign in to comment.