Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -583,9 +583,11 @@ object ResolverBasedAstSchemaBuilder {
case i: Long => i.toDouble
case i: BigInt if !i.isValidDouble => invalidType("Float", value)
case i: BigInt => i.doubleValue
case d: Double => d
case d: BigDecimal if !d.isDecimalDouble => invalidType("Float", value)
case d: BigDecimal => d.doubleValue
case d: Double if java.lang.Double.isFinite(d) => d
case d: Double => invalidType("Float", value)
case d: BigDecimal =>
val dv = d.doubleValue
if (java.lang.Double.isFinite(dv)) dv else invalidType("Float", value)
case v: String => safe(v.toDouble, "Float", value)
case _ => invalidType("Float", value)
}
Expand Down
16 changes: 10 additions & 6 deletions modules/core/src/main/scala/sangria/schema/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,19 @@ package object schema {
case i: Long => Right(i.toDouble)
case i: BigInt if !i.isValidDouble => Left(BigDecimalCoercionViolation)
case i: BigInt => Right(i.doubleValue)
case d: Double => Right(d)
case d: BigDecimal if !d.isDecimalDouble => Left(BigDecimalCoercionViolation)
case d: BigDecimal => Right(d.doubleValue)
case d: Double =>
if (java.lang.Double.isFinite(d)) Right(d) else Left(BigDecimalCoercionViolation)
case d: BigDecimal =>
val dv = d.doubleValue
if (java.lang.Double.isFinite(dv)) Right(dv) else Left(BigDecimalCoercionViolation)
case _ => Left(FloatCoercionViolation)
},
coerceInput = {
case ast.FloatValue(d, _, _) => Right(d)
case ast.BigDecimalValue(d, _, _) if !d.isDecimalDouble => Left(BigDecimalCoercionViolation)
case ast.BigDecimalValue(d, _, _) => Right(d.doubleValue)
case ast.FloatValue(d, _, _) =>
if (java.lang.Double.isFinite(d)) Right(d) else Left(BigDecimalCoercionViolation)
case ast.BigDecimalValue(d, _, _) =>
val dv = d.doubleValue
if (java.lang.Double.isFinite(dv)) Right(dv) else Left(BigDecimalCoercionViolation)
case ast.IntValue(i, _, _) => Right(i)
case ast.BigIntValue(i, _, _) if !i.isValidDouble => Left(BigDecimalCoercionViolation)
case ast.BigIntValue(i, _, _) => Right(i.doubleValue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ case object BigLongCoercionViolation

case object FloatCoercionViolation extends ValueCoercionViolation("Float or Int value expected")
case object BigDecimalCoercionViolation
extends ValueCoercionViolation("Float or Int value is too big to fit in double")
extends ValueCoercionViolation("Value is not a finite Float (IEEE 754 double)")

case object BooleanCoercionViolation extends ValueCoercionViolation("Boolean value expected")
case object StringCoercionViolation extends ValueCoercionViolation("String value expected")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ class ValueCoercionHelperSpec extends AnyWordSpec with Matchers {
check(opt(IDType), "123.456", None)
}

"Float from GraphQL string literal with no exact double (parsed to nearest double)" in {
check(opt(FloatType), "144.75999999999999", Some(Some(144.75999999999999)))
check(opt(FloatType), "0.1", Some(Some(0.1)))
}

val testEnum = EnumType(
"TestColor",
values = List(
Expand Down
62 changes: 62 additions & 0 deletions modules/core/src/test/scala/sangria/execution/VariablesSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ class VariablesSpec extends AnyWordSpec with Matchers with GraphQlSupport {
)
)

val TestInputWithFloat = InputObjectType(
"TestInputWithFloat",
List(InputField("value", FloatType))
)

val TestType = ObjectType(
"TestType",
fields[Unit, Unit](
Expand Down Expand Up @@ -137,6 +142,12 @@ class VariablesSpec extends AnyWordSpec with Matchers with GraphQlSupport {
DefaultValueRenderer.renderCoercedInputValueCompact(
ctx.arg[Any]("input"),
ListInputType(StringType))
),
Field(
"fieldWithFloatInput",
OptionType(StringType),
arguments = Argument("input", TestInputWithFloat) :: Nil,
resolve = ctx => ctx.arg[Any]("input").toString
)
)
)
Expand Down Expand Up @@ -510,6 +521,57 @@ class VariablesSpec extends AnyWordSpec with Matchers with GraphQlSupport {
"""{"input": {"a": "foo", "b": "bar", "c": "baz", "z": "dog"}}""".parseJson,
"""Variable '$input' expected value of type 'TestInputObject' but got: {"a":"foo","b":"bar","c":"baz","z":"dog"}. Reason: 'z' Field 'z' is not defined in the input type 'TestInputObject'."""
)

"parses Float variable with inexact decimal to nearest double" in {
val testQuery =
QueryParser
.parse("""
query Q($input: TestInputWithFloat!) {
fieldWithFloatInput(input: $input)
}
""")
.get
val variables = JsObject(
"input" -> JsObject("value" -> JsNumber(BigDecimal("144.75999999999999")))
)
val result = Executor
.execute(
schema.asInstanceOf[Schema[Unit, Unit]],
testQuery,
variables = variables: JsValue)
.awaitAndRecoverQueryAnalysisScala
.asInstanceOf[Map[String, AnyRef]]
result.get("errors") should equal(None)
val data = result("data").asInstanceOf[Map[String, Any]]
val out = data("fieldWithFloatInput").asInstanceOf[String]
val expectedDouble = 144.75999999999999
out should be(scala.collection.immutable.ListMap("value" -> expectedDouble).toString)
}

"parses Float variable 0.1" in {
val testQuery =
QueryParser
.parse("""
query Q($input: TestInputWithFloat!) {
fieldWithFloatInput(input: $input)
}
""")
.get
val variables = JsObject(
"input" -> JsObject("value" -> JsNumber(BigDecimal("0.1")))
)
val result = Executor
.execute(
schema.asInstanceOf[Schema[Unit, Unit]],
testQuery,
variables = variables: JsValue)
.awaitAndRecoverQueryAnalysisScala
.asInstanceOf[Map[String, AnyRef]]
result.get("errors") should equal(None)
val data = result("data").asInstanceOf[Map[String, Any]]
val out = data("fieldWithFloatInput").asInstanceOf[String]
out should be(scala.collection.immutable.ListMap("value" -> 0.1).toString)
}
}
}

Expand Down
94 changes: 82 additions & 12 deletions modules/core/src/test/scala/sangria/schema/CoercionSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package sangria.schema
import java.util.Date

import sangria.ast._
import sangria.parser.QueryParser
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec

Expand Down Expand Up @@ -30,6 +31,15 @@ class CoercionSpec extends AnyWordSpec with Matchers {
FloatType.coerceOutput(123.456, Set.empty) should be(123.456)
}

"Float preserves 0.1 (not losslessly representable as double)" in {
FloatType.coerceOutput(0.1, Set.empty) should be(0.1)
}

"Float preserves 144.75999999999999 (not losslessly representable as double)" in {
val v = 144.75999999999999
FloatType.coerceOutput(v, Set.empty) should be(v)
}

"Float coerces NaN to null" in {
FloatType.coerceOutput(Double.NaN, Set.empty) should be(null.asInstanceOf[java.lang.Double])
}
Expand Down Expand Up @@ -113,18 +123,27 @@ class CoercionSpec extends AnyWordSpec with Matchers {
.isLeft should be(true)

FloatType.coerceInput(FloatValue(12.34)) should be(Right(12.34d))
FloatType.coerceInput(FloatValue(Double.NaN)).exists(_.isNaN) should be(true)
FloatType
.coerceInput(FloatValue(Double.PositiveInfinity))
.exists(_.isPosInfinity) should be(true)
FloatType
.coerceInput(FloatValue(Double.NegativeInfinity))
.exists(_.isNegInfinity) should be(true)
FloatType.coerceInput(FloatValue(0.1)) should be(Right(0.1))
FloatType.coerceInput(BigDecimalValue(BigDecimal("0.1"))) should be(Right(0.1))
FloatType.coerceInput(FloatValue(144.75999999999999)) should be(Right(144.75999999999999))
FloatType.coerceInput(BigDecimalValue(BigDecimal("144.75999999999999"))) match {
case Right(d) => d should be(144.75999999999999)
case Left(v) =>
fail(s"""BigDecimal("144.75999999999999") coercion MUST succeed, got Left($v)""")
}
FloatType.coerceInput(BigDecimalValue(BigDecimal("144.76"))) match {
case Right(d) => d should be(144.75999999999999)
case Left(v) => fail(s"""BigDecimal("144.76") coercion MUST succeed, got Left($v)""")
}
// Spec §3.5.2: non-finite (NaN, Infinity) must raise request error
FloatType.coerceInput(FloatValue(Double.NaN)).isLeft should be(true)
FloatType.coerceInput(FloatValue(Double.PositiveInfinity)).isLeft should be(true)
FloatType.coerceInput(FloatValue(Double.NegativeInfinity)).isLeft should be(true)
FloatType.coerceInput(BigDecimalValue(BigDecimal(12.34))) should be(Right(12.34d))
FloatType
.coerceInput(
BigDecimalValue(BigDecimal("367476315476516457632.473854635267452376546732")))
.isLeft should be(true)
.isRight should be(true)
FloatType.coerceInput(BooleanValue(true)).isLeft should be(true)
FloatType.coerceInput(StringValue("123")).isLeft should be(true)
}
Expand Down Expand Up @@ -268,13 +287,26 @@ class CoercionSpec extends AnyWordSpec with Matchers {
FloatType.coerceUserInput(BigInt("12323443874982374987329749823")).isLeft should be(true)

FloatType.coerceUserInput(12.34) should be(Right(12.34d))
FloatType.coerceUserInput(Double.NaN).exists(_.isNaN) should be(true)
FloatType.coerceUserInput(Double.PositiveInfinity).exists(_.isPosInfinity) should be(true)
FloatType.coerceUserInput(Double.NegativeInfinity).exists(_.isNegInfinity) should be(true)
FloatType.coerceUserInput(0.1) should be(Right(0.1))
FloatType.coerceUserInput(BigDecimal("0.1")) should be(Right(0.1))
FloatType.coerceUserInput(144.75999999999999) should be(Right(144.75999999999999))
FloatType.coerceUserInput(BigDecimal("144.75999999999999")) match {
case Right(d) => d should be(144.75999999999999)
case Left(v) =>
fail(s"""BigDecimal("144.75999999999999") coercion MUST succeed, got Left($v)""")
}
FloatType.coerceUserInput(BigDecimal("144.76")) match {
case Right(d) => d should be(144.75999999999999)
case Left(v) => fail(s"""BigDecimal("144.76") coercion MUST succeed, got Left($v)""")
}
// Spec §3.5.2: non-finite (NaN, Infinity) must raise request error
FloatType.coerceUserInput(Double.NaN).isLeft should be(true)
FloatType.coerceUserInput(Double.PositiveInfinity).isLeft should be(true)
FloatType.coerceUserInput(Double.NegativeInfinity).isLeft should be(true)
FloatType.coerceUserInput(BigDecimal(12.34)) should be(Right(12.34d))
FloatType
.coerceUserInput(BigDecimal("367476315476516457632.473854635267452376546732"))
.isLeft should be(true)
.isRight should be(true)
FloatType.coerceUserInput(true).isLeft should be(true)
FloatType.coerceUserInput("123").isLeft should be(true)
FloatType.coerceUserInput(new Date).isLeft should be(true)
Expand Down Expand Up @@ -332,4 +364,42 @@ class CoercionSpec extends AnyWordSpec with Matchers {
}
}
}

"Float decimal identity and BigDecimal precision" when {
"144.75999999999999 (Double) equals 144.76 (Double) in Scala" in {
(144.75999999999999: Double) should be(144.76)
}

"GraphQL Float: parsing \"144.75999999999999\" and \"144.76\" yields the same Double" in {
val ast1 = QueryParser.parseInput("144.75999999999999").get
val ast2 = QueryParser.parseInput("144.76").get
val r1 = FloatType.coerceInput(ast1)
val r2 = FloatType.coerceInput(ast2)
r1.isRight should be(true)
r2.isRight should be(true)
r1 should equal(r2)
(r1, r2) should be((Right(144.75999999999999), Right(144.75999999999999)))
}

"BigDecimal preserves exact decimals (144.75999999999999 != 144.76)" in {
BigDecimal("144.75999999999999") should not be BigDecimal("144.76")
BigDecimal("144.75999999999999").toString should be("144.75999999999999")
BigDecimal("144.76").toString should be("144.76")
}

"BigDecimal preserves exact decimal 0.1" in {
BigDecimal("0.1").toString should be("0.1")
}

"GraphQL Float \"0.1\" parses to Double (nearest IEEE 754, not exact decimal 0.1)" in {
val ast = QueryParser.parseInput("0.1").get
val result = FloatType.coerceInput(ast)
result should be(Right(0.1))
val d = result.toOption.get
d should be(0.1)
// 0.1 has no exact binary representation; Double holds the nearest IEEE 754 value
// (exact decimal 0.1 would require BigDecimal; d is slightly != mathematical 0.1)
java.lang.Double.doubleToLongBits(d) should be(java.lang.Double.doubleToLongBits(0.1))
}
}
}