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 8, 2019
1 parent 5424ef7 commit a30814c
Show file tree
Hide file tree
Showing 35 changed files with 964 additions and 172 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
29 changes: 19 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,25 @@ 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"
case OptionSome(ast) => stmt"Some(${ast.token})"
case OptionApply(ast) => stmt"Option(${ast.token})"
case OptionOrNull(ast) => stmt"${ast.token}.orNull"
case OptionOrNullValue(ast) => stmt"${ast.token}.orNullValue"
case OptionNone => stmt"None"
}

implicit def traversableOperationTokenizer(implicit liftTokenizer: Tokenizer[Lift]): Tokenizer[TraversableOperation] = Tokenizer[TraversableOperation] {
Expand Down
9 changes: 9 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,15 @@ 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
object OptionNone extends OptionOperation
case class OptionSome(ast: Ast) extends OptionOperation
case class OptionApply(ast: Ast) extends OptionOperation
case class OptionOrNull(ast: Ast) extends OptionOperation
case class OptionOrNullValue(ast: 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 Expand Up @@ -89,6 +105,19 @@ trait StatefulTransformer[T] {
case OptionIsDefined(a) =>
val (at, att) = apply(a)
(OptionIsDefined(at), att)
case OptionSome(a) =>
val (at, att) = apply(a)
(OptionSome(at), att)
case OptionApply(a) =>
val (at, att) = apply(a)
(OptionApply(at), att)
case OptionOrNull(a) =>
val (at, att) = apply(a)
(OptionOrNull(at), att)
case OptionOrNullValue(a) =>
val (at, att) = apply(a)
(OptionOrNullValue(at), att)
case OptionNone => (o, this)
}

def apply(e: TraversableOperation): (TraversableOperation, StatefulTransformer[T]) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,25 @@ 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))
case OptionSome(a) => OptionSome(apply(a))
case OptionApply(a) => OptionApply(apply(a))
case OptionOrNull(a) => OptionOrNull(apply(a))
case OptionOrNullValue(a) => OptionOrNullValue(apply(a))
case OptionNone => OptionNone
}

def apply(o: TraversableOperation): TraversableOperation =
Expand Down
10 changes: 9 additions & 1 deletion quill-core/src/main/scala/io/getquill/dsl/QueryDsl.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package io.getquill.dsl

import scala.language.experimental.macros

import io.getquill.quotation.NonQuotedException

import scala.annotation.compileTimeOnly

private[dsl] trait QueryDsl {
Expand All @@ -13,6 +13,14 @@ private[dsl] trait QueryDsl {
@compileTimeOnly(NonQuotedException.message)
def querySchema[T](entity: String, columns: (T => (Any, String))*): EntityQuery[T] = NonQuotedException()

implicit class NullableColumnExtensions[A](o: Option[A]) {
@compileTimeOnly(NonQuotedException.message)
def orNullValue: A =
throw new IllegalArgumentException(
"Cannot use orNullValue outside of database queries since only database value-types (e.g. Int, Double, etc...) can be null."
)
}

sealed trait Query[+T] {

def map[R](f: T => R): Query[R]
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,73 @@
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 OptionSome(ast) =>
apply(ast)

case OptionApply(ast) =>
apply(ast)

case OptionOrNull(ast) =>
apply(ast)

case OptionOrNullValue(ast) =>
apply(ast)

case OptionNone => NullValue

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
Loading

0 comments on commit a30814c

Please sign in to comment.