Skip to content

Commit

Permalink
Fix for non-fallthrough null operations in map/flatMap/exists
Browse files Browse the repository at this point in the history
  • Loading branch information
deusaquilus committed Feb 6, 2019
1 parent 5424ef7 commit 982bc47
Show file tree
Hide file tree
Showing 34 changed files with 827 additions and 159 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@ quill-sql/io/
MyTest.scala
MySparkTest.scala
MyTestJdbc.scala
MyTestSql.scala
quill-core/src/main/resources/logback.xml
quill-jdbc/src/main/resources/logback.xml
log.txt*
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package io.getquill.context.cassandra

import io.getquill.ast._
import io.getquill.norm.RenameProperties
import io.getquill.norm.Normalize
import io.getquill.norm.FlattenOptionOperation
import io.getquill.norm.{ FlattenOptionOperation, Normalize, RenameProperties, SimplifyNullChecks }

object CqlNormalize {

Expand All @@ -13,6 +11,7 @@ object CqlNormalize {
private[this] val normalize =
(identity[Ast] _)
.andThen(FlattenOptionOperation.apply _)
.andThen(SimplifyNullChecks.apply _)
.andThen(Normalize.apply _)
.andThen(RenameProperties.apply _)
.andThen(ExpandMappedInfix.apply _)
Expand Down
24 changes: 14 additions & 10 deletions quill-core/src/main/scala/io/getquill/MirrorIdiom.scala
Original file line number Diff line number Diff line change
Expand Up @@ -125,16 +125,20 @@ class MirrorIdiom extends Idiom {
}

implicit def optionOperationTokenizer(implicit liftTokenizer: Tokenizer[Lift]): Tokenizer[OptionOperation] = Tokenizer[OptionOperation] {
case OptionFlatten(ast) => stmt"${ast.token}.flatten"
case OptionGetOrElse(ast, body) => stmt"${ast.token}.getOrElse(${body.token})"
case OptionFlatMap(ast, alias, body) => stmt"${ast.token}.flatMap((${alias.token}) => ${body.token})"
case OptionMap(ast, alias, body) => stmt"${ast.token}.map((${alias.token}) => ${body.token})"
case OptionForall(ast, alias, body) => stmt"${ast.token}.forall((${alias.token}) => ${body.token})"
case OptionExists(ast, alias, body) => stmt"${ast.token}.exists((${alias.token}) => ${body.token})"
case OptionContains(ast, body) => stmt"${ast.token}.contains(${body.token})"
case OptionIsEmpty(ast) => stmt"${ast.token}.isEmpty"
case OptionNonEmpty(ast) => stmt"${ast.token}.nonEmpty"
case OptionIsDefined(ast) => stmt"${ast.token}.isDefined"
case UncheckedOptionFlatMap(ast, alias, body) => stmt"${ast.token}.flatMap((${alias.token}) => ${body.token})"
case UncheckedOptionMap(ast, alias, body) => stmt"${ast.token}.map((${alias.token}) => ${body.token})"
case UncheckedOptionExists(ast, alias, body) => stmt"${ast.token}.exists((${alias.token}) => ${body.token})"
case UncheckedOptionForall(ast, alias, body) => stmt"${ast.token}.forall((${alias.token}) => ${body.token})"
case OptionFlatten(ast) => stmt"${ast.token}.flatten"
case OptionGetOrElse(ast, body) => stmt"${ast.token}.getOrElse(${body.token})"
case OptionFlatMap(ast, alias, body) => stmt"${ast.token}.flatMap((${alias.token}) => ${body.token})"
case OptionMap(ast, alias, body) => stmt"${ast.token}.map((${alias.token}) => ${body.token})"
case OptionForall(ast, alias, body) => stmt"${ast.token}.forall((${alias.token}) => ${body.token})"
case OptionExists(ast, alias, body) => stmt"${ast.token}.exists((${alias.token}) => ${body.token})"
case OptionContains(ast, body) => stmt"${ast.token}.contains(${body.token})"
case OptionIsEmpty(ast) => stmt"${ast.token}.isEmpty"
case OptionNonEmpty(ast) => stmt"${ast.token}.nonEmpty"
case OptionIsDefined(ast) => stmt"${ast.token}.isDefined"
}

implicit def traversableOperationTokenizer(implicit liftTokenizer: Tokenizer[Lift]): Tokenizer[TraversableOperation] = Tokenizer[TraversableOperation] {
Expand Down
4 changes: 4 additions & 0 deletions quill-core/src/main/scala/io/getquill/ast/Ast.scala
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ case class OptionContains(ast: Ast, body: Ast) extends OptionOperation
case class OptionIsEmpty(ast: Ast) extends OptionOperation
case class OptionNonEmpty(ast: Ast) extends OptionOperation
case class OptionIsDefined(ast: Ast) extends OptionOperation
case class UncheckedOptionFlatMap(ast: Ast, alias: Ident, body: Ast) extends OptionOperation
case class UncheckedOptionMap(ast: Ast, alias: Ident, body: Ast) extends OptionOperation
case class UncheckedOptionExists(ast: Ast, alias: Ident, body: Ast) extends OptionOperation
case class UncheckedOptionForall(ast: Ast, alias: Ident, body: Ast) extends OptionOperation

sealed trait TraversableOperation extends Ast
case class MapContains(ast: Ast, body: Ast) extends TraversableOperation
Expand Down
78 changes: 78 additions & 0 deletions quill-core/src/main/scala/io/getquill/ast/AstOps.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package io.getquill.ast

object Implicits {
implicit class AstOpsExt(body: Ast) {
def +||+(other: Ast) = BinaryOperation(body, BooleanOperator.`||`, other)
def +&&+(other: Ast) = BinaryOperation(body, BooleanOperator.`&&`, other)
def +==+(other: Ast) = BinaryOperation(body, EqualityOperator.`==`, other)
}
}

object +||+ {
def unapply(a: Ast): Option[(Ast, Ast)] = {
a match {
case BinaryOperation(one, BooleanOperator.`||`, two) => Some((one, two))
case _ => None
}
}
}

object +&&+ {
def unapply(a: Ast): Option[(Ast, Ast)] = {
a match {
case BinaryOperation(one, BooleanOperator.`&&`, two) => Some((one, two))
case _ => None
}
}
}

object +==+ {
def unapply(a: Ast): Option[(Ast, Ast)] = {
a match {
case BinaryOperation(one, EqualityOperator.`==`, two) => Some((one, two))
case _ => None
}
}
}

object IsNotNullCheck {
def apply(ast: Ast) = BinaryOperation(ast, EqualityOperator.`!=`, NullValue)

def unapply(ast: Ast): Option[Ast] = {
ast match {
case BinaryOperation(cond, EqualityOperator.`!=`, NullValue) => Some(cond)
case _ => None
}
}
}

object IsNullCheck {
def apply(ast: Ast) = BinaryOperation(ast, EqualityOperator.`==`, NullValue)

def unapply(ast: Ast): Option[Ast] = {
ast match {
case BinaryOperation(cond, EqualityOperator.`==`, NullValue) => Some(cond)
case _ => None
}
}
}

object IfExistElseNull {
def apply(exists: Ast, `then`: Ast) =
If(IsNotNullCheck(exists), `then`, NullValue)

def unapply(ast: Ast) = ast match {
case If(IsNotNullCheck(exists), t, NullValue) => Some((exists, t))
case _ => None
}
}

object IfExist {
def apply(exists: Ast, `then`: Ast, otherwise: Ast) =
If(IsNotNullCheck(exists), `then`, otherwise)

def unapply(ast: Ast) = ast match {
case If(IsNotNullCheck(exists), t, e) => Some((exists, t, e))
case _ => None
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,22 @@ trait StatefulTransformer[T] {

def apply(o: OptionOperation): (OptionOperation, StatefulTransformer[T]) =
o match {
case UncheckedOptionFlatMap(a, b, c) =>
val (at, att) = apply(a)
val (ct, ctt) = att.apply(c)
(UncheckedOptionFlatMap(at, b, ct), ctt)
case UncheckedOptionMap(a, b, c) =>
val (at, att) = apply(a)
val (ct, ctt) = att.apply(c)
(UncheckedOptionMap(at, b, ct), ctt)
case UncheckedOptionExists(a, b, c) =>
val (at, att) = apply(a)
val (ct, ctt) = att.apply(c)
(UncheckedOptionExists(at, b, ct), ctt)
case UncheckedOptionForall(a, b, c) =>
val (at, att) = apply(a)
val (ct, ctt) = att.apply(c)
(UncheckedOptionForall(at, b, ct), ctt)
case OptionFlatten(a) =>
val (at, att) = apply(a)
(OptionFlatten(at), att)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,20 @@ trait StatelessTransformer {

def apply(o: OptionOperation): OptionOperation =
o match {
case OptionFlatten(a) => OptionFlatten(apply(a))
case OptionGetOrElse(a, b) => OptionGetOrElse(apply(a), apply(b))
case OptionFlatMap(a, b, c) => OptionFlatMap(apply(a), b, apply(c))
case OptionMap(a, b, c) => OptionMap(apply(a), b, apply(c))
case OptionForall(a, b, c) => OptionForall(apply(a), b, apply(c))
case OptionExists(a, b, c) => OptionExists(apply(a), b, apply(c))
case OptionContains(a, b) => OptionContains(apply(a), apply(b))
case OptionIsEmpty(a) => OptionIsEmpty(apply(a))
case OptionNonEmpty(a) => OptionNonEmpty(apply(a))
case OptionIsDefined(a) => OptionIsDefined(apply(a))
case UncheckedOptionFlatMap(a, b, c) => UncheckedOptionFlatMap(apply(a), b, apply(c))
case UncheckedOptionMap(a, b, c) => UncheckedOptionMap(apply(a), b, apply(c))
case UncheckedOptionExists(a, b, c) => UncheckedOptionExists(apply(a), b, apply(c))
case UncheckedOptionForall(a, b, c) => UncheckedOptionForall(apply(a), b, apply(c))
case OptionFlatten(a) => OptionFlatten(apply(a))
case OptionGetOrElse(a, b) => OptionGetOrElse(apply(a), apply(b))
case OptionFlatMap(a, b, c) => OptionFlatMap(apply(a), b, apply(c))
case OptionMap(a, b, c) => OptionMap(apply(a), b, apply(c))
case OptionForall(a, b, c) => OptionForall(apply(a), b, apply(c))
case OptionExists(a, b, c) => OptionExists(apply(a), b, apply(c))
case OptionContains(a, b) => OptionContains(apply(a), apply(b))
case OptionIsEmpty(a) => OptionIsEmpty(apply(a))
case OptionNonEmpty(a) => OptionNonEmpty(apply(a))
case OptionIsDefined(a) => OptionIsDefined(apply(a))
}

def apply(o: TraversableOperation): TraversableOperation =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ case class BetaReduction(map: collection.Map[Ast, Ast])

override def apply(o: OptionOperation) =
o match {
case other @ UncheckedOptionFlatMap(a, b, c) =>
UncheckedOptionFlatMap(apply(a), b, BetaReduction(map - b)(c))
case UncheckedOptionMap(a, b, c) =>
UncheckedOptionMap(apply(a), b, BetaReduction(map - b)(c))
case UncheckedOptionExists(a, b, c) =>
UncheckedOptionExists(apply(a), b, BetaReduction(map - b)(c))
case UncheckedOptionForall(a, b, c) =>
UncheckedOptionForall(apply(a), b, BetaReduction(map - b)(c))
case other @ OptionFlatMap(a, b, c) =>
OptionFlatMap(apply(a), b, BetaReduction(map - b)(c))
case OptionMap(a, b, c) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,59 @@
package io.getquill.norm

import io.getquill.ast._
import io.getquill.ast.Implicits._

object FlattenOptionOperation extends StatelessTransformer {

private def isNotEmpty(ast: Ast) =
BinaryOperation(ast, EqualityOperator.`!=`, NullValue)

private def emptyOrNot(b: Boolean, ast: Ast) =
if (b) OptionIsEmpty(ast) else OptionNonEmpty(ast)

override def apply(ast: Ast): Ast =
ast match {

// TODO Check if there is an optional in here, if there is, warn the user about changing behavior

case UncheckedOptionFlatMap(ast, alias, body) =>
apply(BetaReduction(body, alias -> ast))

case UncheckedOptionMap(ast, alias, body) =>
apply(BetaReduction(body, alias -> ast))

case UncheckedOptionExists(ast, alias, body) =>
apply(BetaReduction(body, alias -> ast))

case UncheckedOptionForall(ast, alias, body) =>
val reduced = BetaReduction(body, alias -> ast)
apply((IsNullCheck(ast) +||+ reduced): Ast)

case OptionFlatten(ast) =>
apply(ast)

case OptionGetOrElse(OptionMap(ast, alias, body), Constant(b: Boolean)) =>
apply(BinaryOperation(BetaReduction(body, alias -> ast), BooleanOperator.`||`, emptyOrNot(b, ast)): Ast)
apply((BetaReduction(body, alias -> ast) +||+ emptyOrNot(b, ast)): Ast)

case OptionGetOrElse(ast, body) =>
apply(If(isNotEmpty(ast), ast, body))
apply(If(IsNotNullCheck(ast), ast, body))

case OptionFlatMap(ast, alias, body) =>
apply(BetaReduction(body, alias -> ast))
val reduced = BetaReduction(body, alias -> ast)
apply(IfExistElseNull(ast, reduced))

case OptionMap(ast, alias, body) =>
apply(BetaReduction(body, alias -> ast))
val reduced = BetaReduction(body, alias -> ast)
apply(IfExistElseNull(ast, reduced))

case OptionForall(ast, alias, body) =>
val isEmpty = BinaryOperation(ast, EqualityOperator.`==`, NullValue)
val exists = BetaReduction(body, alias -> ast)
apply(BinaryOperation(isEmpty, BooleanOperator.`||`, exists): Ast)
val reduction = BetaReduction(body, alias -> ast)
apply((IsNullCheck(ast) +||+ (IsNotNullCheck(ast) +&&+ reduction)): Ast)

case OptionExists(ast, alias, body) =>
apply(BetaReduction(body, alias -> ast))
val reduction = BetaReduction(body, alias -> ast)
apply((IsNotNullCheck(ast) +&&+ reduction): Ast)

case OptionContains(ast, body) =>
apply(BinaryOperation(ast, EqualityOperator.`==`, body): Ast)
apply((ast +==+ body): Ast)

case other =>
super.apply(other)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package io.getquill.norm

import io.getquill.ast._
import io.getquill.ast.Implicits._

/**
* Due to the introduction of null checks in `map`, `flatMap`, and `exists`, in
* `FlattenOptionOperation` in order to resolve #1053, as well as to support non-ansi
* compliant string concatenation as outlined in #1295, large conditional composites
* became common. For example:
* <code><pre>
* case class Holder(value:Option[String])
*
* // The following statement
* query[Holder].map(h => h.value.map(_ + "foo"))
* // Will yield the following result
* SELECT CASE WHEN h.value IS NOT NULL THEN h.value || 'foo' ELSE null END FROM Holder h
* </pre></code>
* Now, let's add a <code>getOrElse</code> statement to the clause that requires an additional
* wrapped null check. We cannot rely on there being a <code>map</code> call beforehand
* since we could be reading <code>value</code> as a nullable field directly from the database).
* <code><pre>
* // The following statement
* query[Holder].map(h => h.value.map(_ + "foo").getOrElse("bar"))
* // Yields the following result:
* SELECT CASE WHEN
* CASE WHEN h.value IS NOT NULL THEN h.value || 'foo' ELSE null END
* IS NOT NULL THEN
* CASE WHEN h.value IS NOT NULL THEN h.value || 'foo' ELSE null END
* ELSE 'bar' END FROM Holder h
* </pre></code>
* This of course is highly redundant and can be reduced to simply:
* <code><pre>
* SELECT CASE WHEN h.value IS NOT NULL AND (h.value || 'foo') IS NOT NULL THEN h.value || 'foo' ELSE 'bar' END FROM Holder h
* </pre></code>
* This reduction is done by the "Center Rule." There are some other simplification
* rules as well. Note how we are force to null-check both `h.value` as well as `(h.value || 'foo')` because
* a user may use `Option[T].flatMap` and explicitly transform a particular value to `null`.
*/
object SimplifyNullChecks extends StatelessTransformer {

override def apply(ast: Ast): Ast =
ast match {

// Center rule
case IfExist(
IfExistElseNull(condA, thenA),
IfExistElseNull(condB, thenB),
otherwise
) if (condA == condB && thenA == thenB) =>
apply(If(IsNotNullCheck(condA) +&&+ IsNotNullCheck(thenA), thenA, otherwise))

// Left hand rule
case IfExist(IfExistElseNull(check, affirm), value, otherwise) =>
apply(If(IsNotNullCheck(check) +&&+ IsNotNullCheck(affirm), value, otherwise))

// Right hand rule
case IfExistElseNull(cond, IfExistElseNull(innerCond, innerThen)) =>
apply(If(IsNotNullCheck(cond) +&&+ IsNotNullCheck(innerCond), innerThen, NullValue))

case other =>
super.apply(other)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ case class FreeVariables(state: State)

override def apply(o: OptionOperation): (OptionOperation, StatefulTransformer[State]) =
o match {
case q @ UncheckedOptionFlatMap(a, b, c) =>
(q, free(a, b, c))
case q @ UncheckedOptionMap(a, b, c) =>
(q, free(a, b, c))
case q @ UncheckedOptionExists(a, b, c) =>
(q, free(a, b, c))
case q @ UncheckedOptionForall(a, b, c) =>
(q, free(a, b, c))
case q @ OptionFlatMap(a, b, c) =>
(q, free(a, b, c))
case q @ OptionMap(a, b, c) =>
Expand Down
Loading

0 comments on commit 982bc47

Please sign in to comment.