From e32904987c441c24428ac77511b8000194b7e2ce Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Wed, 22 Nov 2023 18:40:46 +1100 Subject: [PATCH 1/3] Use dedicated methods for steps in executor --- .../scala/caliban/execution/Executor.scala | 199 +++++++++--------- 1 file changed, 103 insertions(+), 96 deletions(-) diff --git a/core/src/main/scala/caliban/execution/Executor.scala b/core/src/main/scala/caliban/execution/Executor.scala index 1896e9fdc5..48f4608b61 100644 --- a/core/src/main/scala/caliban/execution/Executor.scala +++ b/core/src/main/scala/caliban/execution/Executor.scala @@ -11,7 +11,8 @@ import caliban.schema.Step._ import caliban.schema.{ ReducedStep, Step, Types } import caliban.wrappers.Wrapper.FieldWrapper import zio._ -import zio.query.{ Cache, ZQuery } +import zio.query.{ Cache, UQuery, URQuery, ZQuery } +import zio.stacktracer.TracingImplicits.disableAutoTrace import zio.stream.ZStream import scala.annotation.tailrec @@ -35,6 +36,7 @@ object Executor { featureSet: Set[Feature] = Set.empty )(implicit trace: Trace): URIO[R, GraphQLResponse[CalibanError]] = { val wrapPureValues = fieldWrappers.exists(_.wrapPureValues) + type ExecutionQuery[+A] = ZQuery[R, ExecutionError, A] val execution = request.operationType match { case OperationType.Query => queryExecution @@ -53,7 +55,55 @@ object Executor { currentField: Field, arguments: Map[String, InputValue], path: List[Either[String, Int]] - ): ReducedStep[R] = + ): ReducedStep[R] = { + + def reduceObjectStep(objectName: String, fields: Map[String, Step[R]]) = { + val filteredFields = mergeFields(currentField, objectName) + val (deferred, eager) = filteredFields.partitionMap { + case f @ Field("__typename", _, _, _, _, _, _, directives, _, _, _) => + Right((f.aliasedName, PureStep(StringValue(objectName)), fieldInfo(f, path, directives))) + case f @ Field(name, _, _, _, _, _, args, directives, _, _, fragment) => + val aliasedName = f.aliasedName + val field = fields + .get(name) + .fold(NullStep: ReducedStep[R])(reduceStep(_, f, args, Left(aliasedName) :: path)) + + val info = fieldInfo(f, path, directives) + + fragment.collectFirst { + // The defer spec provides some latitude on how we handle responses. Since it is more performant to return + // pure fields rather than spin up the defer machinery we return pure fields immediately to the caller. + case IsDeferred(label) if featureSet(Feature.Defer) && !field.isPure => + (label, (aliasedName, field, info)) + }.toLeft((aliasedName, field, info)) + } + + deferred match { + case Nil => reduceObject(eager, wrapPureValues) + case d => + DeferStep( + reduceObject(eager, wrapPureValues), + d.groupBy(_._1).toList.map { case (label, labelAndFields) => + val (_, fields) = labelAndFields.unzip + reduceObject(fields, wrapPureValues) -> label + }, + path + ) + } + } + + def reduceListStep(steps: List[Step[R]]) = { + var i = 0 + val lb = List.newBuilder[ReducedStep[R]] + var remaining = steps + while (!remaining.isEmpty) { + lb += reduceStep(remaining.head, currentField, arguments, Right(i) :: path) + i += 1 + remaining = remaining.tail + } + reduceList(lb.result(), Types.listOf(currentField.fieldType).fold(false)(_.isNullable)) + } + step match { case s @ PureStep(EnumValue(v)) => // special case of an hybrid union containing case objects, those should return an object instead of a string @@ -67,39 +117,6 @@ object Executor { case s: PureStep => s case FunctionStep(step) => reduceStep(step(arguments), currentField, Map.empty, path) case MetadataFunctionStep(step) => reduceStep(step(currentField), currentField, arguments, path) - case ObjectStep(objectName, fields) => - val filteredFields = mergeFields(currentField, objectName) - val (deferred, eager) = filteredFields.partitionMap { - case f @ Field("__typename", _, _, _, _, _, _, directives, _, _, _) => - Right((f.aliasedName, PureStep(StringValue(objectName)), fieldInfo(f, path, directives))) - case f @ Field(name, _, _, _, _, _, args, directives, _, _, fragment) => - val aliasedName = f.aliasedName - val field = fields - .get(name) - .fold(NullStep: ReducedStep[R])(reduceStep(_, f, args, Left(aliasedName) :: path)) - - val info = fieldInfo(f, path, directives) - - fragment.collectFirst { - // The defer spec provides some latitude on how we handle responses. Since it is more performant to return - // pure fields rather than spin up the defer machinery we return pure fields immediately to the caller. - case IsDeferred(label) if featureSet(Feature.Defer) && !field.isPure => - (label, (aliasedName, field, info)) - }.toLeft((aliasedName, field, info)) - } - - deferred match { - case Nil => reduceObject(eager, wrapPureValues) - case d => - DeferStep( - reduceObject(eager, wrapPureValues), - d.groupBy(_._1).toList.map { case (label, labelAndFields) => - val (_, fields) = labelAndFields.unzip - reduceObject(fields, wrapPureValues) -> label - }, - path - ) - } case QueryStep(inner) => ReducedStep.QueryStep( inner.foldCauseQuery( @@ -107,16 +124,8 @@ object Executor { a => ZQuery.succeed(reduceStep(a, currentField, arguments, path)) ) ) - case ListStep(steps) => - var i = 0 - val lb = List.newBuilder[ReducedStep[R]] - var remaining = steps - while (!remaining.isEmpty) { - lb += reduceStep(remaining.head, currentField, arguments, Right(i) :: path) - i += 1 - remaining = remaining.tail - } - reduceList(lb.result(), Types.listOf(currentField.fieldType).fold(false)(_.isNullable)) + case ObjectStep(objectName, fields) => reduceObjectStep(objectName, fields) + case ListStep(steps) => reduceListStep(steps) case StreamStep(stream) => if (request.operationType == OperationType.Subscription) { ReducedStep.StreamStep( @@ -133,79 +142,78 @@ object Executor { ) } } + } def makeQuery( step: ReducedStep[R], errors: Ref[List[CalibanError]], deferred: Ref[List[Deferred[R]]] - ): ZQuery[R, Nothing, ResponseValue] = { + ): URQuery[R, ResponseValue] = { - def handleError(error: ExecutionError): ZQuery[Any, Nothing, ResponseValue] = + def handleError(error: ExecutionError): UQuery[ResponseValue] = ZQuery.fromZIO(errors.update(error :: _)).as(NullValue) - def wrap(query: ZQuery[R, ExecutionError, ResponseValue], isPure: Boolean)( - wrappers: List[FieldWrapper[R]], - fieldInfo: FieldInfo - ): ZQuery[R, ExecutionError, ResponseValue] = { + def wrap(query: ExecutionQuery[ResponseValue], isPure: Boolean, fieldInfo: FieldInfo) = { @tailrec - def loop( - query: ZQuery[R, ExecutionError, ResponseValue], - wrappers: List[FieldWrapper[R]] - ): ZQuery[R, ExecutionError, ResponseValue] = + def loop(query: ExecutionQuery[ResponseValue], wrappers: List[FieldWrapper[R]]): ExecutionQuery[ResponseValue] = wrappers match { case Nil => query case wrapper :: tail => val q = if (isPure && !wrapper.wrapPureValues) query else wrapper.wrap(query, fieldInfo) loop(q, tail) } - loop(query, wrappers) + loop(query, fieldWrappers) } def objectFieldQuery(name: String, step: ReducedStep[R], info: FieldInfo) = { - val q = wrap(loop(step), step.isPure)(fieldWrappers, info) + val q = wrap(loop(step), step.isPure, info) + if (info.details.fieldType.isNullable) q.catchAll(handleError).map((name, _)) else q.map((name, _)) } - def loop(step: ReducedStep[R]): ZQuery[R, ExecutionError, ResponseValue] = - step match { - case PureStep(value) => ZQuery.succeed(value) - case ReducedStep.ObjectStep(steps) => - var resolved: mutable.HashMap[String, ResponseValue] = null - - val queries = - if (wrapPureValues) steps.map((objectFieldQuery _).tupled) - else { - val queries = List.newBuilder[ZQuery[R, ExecutionError, (String, ResponseValue)]] - - var remaining = steps - while (!remaining.isEmpty) { - remaining.head match { - case (name, PureStep(value), _) => - if (null == resolved) resolved = new mutable.HashMap[String, ResponseValue]() - resolved.update(name, value) - case (name, step, info) => - queries += objectFieldQuery(name, step, info) - } - remaining = remaining.tail - } - queries.result() + def makeObjectQuery(steps: List[(String, ReducedStep[R], FieldInfo)]) = { + var resolved: mutable.HashMap[String, ResponseValue] = null + + val queries = + if (wrapPureValues) steps.map((objectFieldQuery _).tupled) + else { + val queries = List.newBuilder[ExecutionQuery[(String, ResponseValue)]] + var remaining = steps + + while (!remaining.isEmpty) { + remaining.head match { + case (name, PureStep(value), _) => + if (null == resolved) resolved = new mutable.HashMap[String, ResponseValue]() + resolved.update(name, value) + case (name, step, info) => + queries += objectFieldQuery(name, step, info) } + remaining = remaining.tail + } + queries.result() + } - if (null == resolved) collectAll(queries).map(ObjectValue.apply) - else - collectAll(queries).map { results => - results.foreach(kv => resolved.update(kv._1, kv._2)) - ObjectValue(steps.map { case (name, _, _) => name -> resolved(name) }) - } - case ReducedStep.QueryStep(step) => - step.flatMap(loop) - case ReducedStep.ListStep(steps, areItemsNullable) => - val queries = - if (areItemsNullable) steps.map(loop(_).catchAll(handleError)) - else steps.map(loop) - - collectAll(queries).map(ListValue.apply) + if (null == resolved) collectAll(queries).map(ObjectValue.apply) + else + collectAll(queries).map { results => + results.foreach(kv => resolved.update(kv._1, kv._2)) + ObjectValue(steps.map { case (name, _, _) => name -> resolved(name) }) + } + } + + def makeListQuery(steps: List[ReducedStep[R]], areItemsNullable: Boolean) = + collectAll( + if (areItemsNullable) steps.map(loop(_).catchAll(handleError)) + else steps.map(loop) + ).map(ListValue.apply) + + def loop(step: ReducedStep[R]): ExecutionQuery[ResponseValue] = + step match { + case PureStep(value) => ZQuery.succeed(value) + case ReducedStep.QueryStep(step) => step.flatMap(loop) + case ReducedStep.ObjectStep(steps) => makeObjectQuery(steps) + case ReducedStep.ListStep(steps, areItemsNullable) => makeListQuery(steps, areItemsNullable) case ReducedStep.StreamStep(stream) => ZQuery .environment[R] @@ -295,11 +303,10 @@ object Executor { } yield response } - private[caliban] def fail(error: CalibanError): UIO[GraphQLResponse[CalibanError]] = + private[caliban] def fail(error: CalibanError)(implicit trace: Trace): UIO[GraphQLResponse[CalibanError]] = ZIO.succeed(GraphQLResponse(NullValue, List(error))) private[caliban] def mergeFields(field: Field, typeName: String): List[Field] = { - // ugly mutable code but it's worth it for the speed ;) val map = new java.util.LinkedHashMap[String, Field]() var modified = false From 6dcfdc32af6b3f9518d319e348bebb3e41a3421f Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Wed, 22 Nov 2023 18:48:55 +1100 Subject: [PATCH 2/3] Remove implicit requirement to make mima happy --- core/src/main/scala/caliban/execution/Executor.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/scala/caliban/execution/Executor.scala b/core/src/main/scala/caliban/execution/Executor.scala index 48f4608b61..f7b5c47491 100644 --- a/core/src/main/scala/caliban/execution/Executor.scala +++ b/core/src/main/scala/caliban/execution/Executor.scala @@ -303,7 +303,7 @@ object Executor { } yield response } - private[caliban] def fail(error: CalibanError)(implicit trace: Trace): UIO[GraphQLResponse[CalibanError]] = + private[caliban] def fail(error: CalibanError): UIO[GraphQLResponse[CalibanError]] = ZIO.succeed(GraphQLResponse(NullValue, List(error))) private[caliban] def mergeFields(field: Field, typeName: String): List[Field] = { From 888ddf5c52d3667e503862d3c3af20736c400a51 Mon Sep 17 00:00:00 2001 From: Kyri Petrou Date: Wed, 22 Nov 2023 18:56:36 +1100 Subject: [PATCH 3/3] Don't disable auto-tracing --- core/src/main/scala/caliban/execution/Executor.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/scala/caliban/execution/Executor.scala b/core/src/main/scala/caliban/execution/Executor.scala index f7b5c47491..87a3b13409 100644 --- a/core/src/main/scala/caliban/execution/Executor.scala +++ b/core/src/main/scala/caliban/execution/Executor.scala @@ -12,7 +12,6 @@ import caliban.schema.{ ReducedStep, Step, Types } import caliban.wrappers.Wrapper.FieldWrapper import zio._ import zio.query.{ Cache, UQuery, URQuery, ZQuery } -import zio.stacktracer.TracingImplicits.disableAutoTrace import zio.stream.ZStream import scala.annotation.tailrec