![JAR search and dependency download from the Maven repository](/logo.png)
caliban.execution.Executor.scala Maven / Gradle / Ivy
package caliban.execution
import caliban.CalibanError.ExecutionError
import caliban.ResponseValue._
import caliban.Value._
import caliban._
import caliban.execution.Fragment.IsDeferred
import caliban.parsing.adt._
import caliban.schema.ReducedStep.DeferStep
import caliban.schema.Step.{ PureStep => _, _ }
import caliban.schema.{ PureStep, ReducedStep, Step, Types }
import caliban.transformers.Transformer
import caliban.wrappers.Wrapper.FieldWrapper
import zio._
import zio.query._
import zio.stacktracer.TracingImplicits.disableAutoTrace
import zio.stream.{ ZSink, ZStream }
import java.util.concurrent.atomic.AtomicReference
import scala.annotation.{ switch, tailrec }
import scala.collection.compat.{ BuildFrom => _, _ }
import scala.collection.immutable.VectorBuilder
import scala.collection.mutable.ListBuffer
import scala.jdk.CollectionConverters._
import scala.util.control.NonFatal
object Executor {
/**
* Executes the given query against a schema. It returns either an [[caliban.CalibanError.ExecutionError]] or a [[ResponseValue]].
* @param request a request object containing all information needed
* @param plan an execution plan
* @param fieldWrappers a list of field wrappers
* @param queryExecution a strategy for executing queries in parallel or not
* @param makeCache effect used to create a new cache for the query execution
*/
def executeRequest[R](
request: ExecutionRequest,
plan: Step[R],
fieldWrappers: List[FieldWrapper[R]] = Nil,
queryExecution: QueryExecution = QueryExecution.Parallel,
featureSet: Set[Feature] = Set.empty,
makeCache: UIO[Cache] = Cache.empty(Trace.empty),
transformer: Transformer[R] = Transformer.empty[R]
)(implicit trace: Trace): URIO[R, GraphQLResponse[CalibanError]] = {
val wrapPureValues = fieldWrappers.exists(_.wrapPureValues)
val featureFlags = featureSet.foldLeft(0)(_ | _.mask)
val stepReducer =
new StepReducer[R](
transformer,
request.operationType eq OperationType.Subscription,
wrapPureValues = wrapPureValues,
featureFlags
)
val reducedStepExecutor =
new ReducedStepExecutor[R](
queryExecution,
request.operationType eq OperationType.Mutation,
fieldWrappers,
wrapPureValues
)
def runQuery(step: ReducedStep[R], cache: Cache) = {
val deferred = new AtomicReference(List.empty[Deferred[R]])
val errors = new AtomicReference(List.empty[CalibanError])
reducedStepExecutor
.makeQuery(step, errors, deferred)
.runCache(cache)
.flatMap { result =>
val resultErrors = errors.get().reverse
val defers = deferred.get()
if (defers.nonEmpty) {
ZIO.environmentWith[R] { env =>
val stream = (makeDeferStream(defers, cache)
.mapChunks(chunk => Chunk.single(GraphQLIncrementalResponse(chunk.toList, hasNext = true))) ++ ZStream
.succeed(GraphQLIncrementalResponse.empty))
.map(_.toResponseValue)
.provideEnvironment(env)
GraphQLResponse(
StreamValue(ZStream.succeed(result) ++ stream),
resultErrors,
hasNext = Some(true)
)
}
} else
Exit.succeed(GraphQLResponse(result, resultErrors, hasNext = None))
}
}
def makeDeferStream(
defers: List[Deferred[R]],
cache: Cache
): ZStream[R, Nothing, Incremental[CalibanError]] = {
def runDefer(d: DeferredFragment[R]) =
ZStream.unwrap(runIncrementalQuery(d.step, cache).map { case (result, errors, more) =>
val resp = ZStream.succeed(
Incremental.Defer(
result,
errors = errors,
path = ListValue(d.path.reverse),
label = d.label
)
)
more match {
case Nil => resp
case _ => resp ++ makeDeferStream(more, cache)
}
})
def runStream(d: DeferredStream[R]) =
d.step.inner.orDie.chunks.flatMap { steps =>
ZStream.unwrap(ZIO.foreach(steps)(runIncrementalQuery(_, cache)).map { results =>
val (values, errors, more) = results.unzip3
val resp = ZStream.succeed(
Incremental.Stream(
values.toList,
ListValue((IntValue(d.startFrom) :: d.path).reverse),
errors.toList.flatten,
d.label
)
)
more.toList.flatten match {
case Nil => resp
case rest => resp ++ makeDeferStream(rest, cache)
}
})
}
ZStream.mergeAllUnbounded()(defers.map {
case d: DeferredFragment[R] => runDefer(d)
case d: DeferredStream[R] => runStream(d)
}: _*)
}
def runIncrementalQuery(
step: ReducedStep[R],
cache: Cache
) = {
val deferred = new AtomicReference(List.empty[Deferred[R]])
val errors = new AtomicReference(List.empty[CalibanError])
reducedStepExecutor
.makeQuery(step, errors, deferred)
.runCache(cache)
.map { result =>
(
result,
errors.get().reverse,
deferred.get()
)
}
}
ZIO.suspendSucceed {
stepReducer.reduceStep(plan, request.field, Map.empty, Nil) match {
case PureStep(resp) => Exit.succeed(GraphQLResponse(resp, Nil))
case reducedStep => makeCache.flatMap(runQuery(reducedStep, _))
}
}
}
private[caliban] def fail(error: CalibanError): UIO[GraphQLResponse[CalibanError]] =
Exit.succeed(GraphQLResponse(NullValue, List(error)))
private final class StepReducer[R](
transformer: Transformer[R],
isSubscription: Boolean,
wrapPureValues: Boolean,
flags: Feature.Flags
)(implicit trace: Trace) {
def reduceStep(
step: Step[R],
currentField: Field,
arguments: Map[String, InputValue],
path: List[PathValue]
): ReducedStep[R] = {
def reduceObjectStep(objectName: String, getFieldStep: String => Step[R]): ReducedStep[R] = {
def reduceField(f: Field): (String, ReducedStep[R], FieldInfo) = {
val aliasedName = f.aliasedName
val fname = f.name
val field =
if (fname == "__typename") PureStep(StringValue(objectName))
else reduceStep(getFieldStep(fname), f, f.arguments, PathValue.Key(aliasedName) :: path)
(aliasedName, field, fieldInfo(f, aliasedName, path, f.directives))
}
val filteredFields = mergeFields(currentField, objectName)
val (deferred, eager) =
if (Feature.isDeferEnabled(flags)) {
filteredFields.partitionMap { f =>
val entry = reduceField(f)
f.fragment match {
// 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 Some(IsDeferred(label)) if !entry._2.isPure => Left((label, entry))
case _ => Right(entry)
}
}
} else (Nil, filteredFields.map(reduceField))
val eagerReduced = reduceObject(eager)
deferred match {
case Nil => eagerReduced
case d =>
DeferStep(
eagerReduced,
d.groupBy(_._1).toList.map { case (label, labelAndFields) =>
val (_, fields) = labelAndFields.unzip
reduceObject(fields) -> label
},
path
)
}
}
def reduceListStep(steps: List[Step[R]]): ReducedStep[R] = {
def reduceToListStep(head: ReducedStep[R], remaining0: List[Step[R]]): ReducedStep[R] = {
var i = 1
val nil = Nil
val lb = ListBuffer.empty[ReducedStep[R]]
var isPure = wrapPureValues && head.isPure
lb addOne head
var remaining = remaining0
while (remaining ne nil) {
val step = reduceStep(remaining.head, currentField, arguments, PathValue.Index(i) :: path)
if (isPure && !step.isPure) isPure = false
lb addOne step
i += 1
remaining = remaining.tail
}
ReducedStep.ListStep(
lb.result(),
Types.listOf(currentField.fieldType) match {
case Some(tpe) => tpe.isNullable
case None => false
},
isPure
)
}
def reduceToPureStep(head: PureStep, remaining0: List[Step[R]]): ReducedStep[R] = {
var i = 1
val nil = Nil
val lb = ListBuffer.empty[ResponseValue]
var remaining = remaining0
lb addOne head.value
while (remaining ne nil) {
val step = reduceStep(remaining.head, currentField, arguments, PathValue.Index(i) :: path)
lb addOne step.asInstanceOf[PureStep].value
i += 1
remaining = remaining.tail
}
PureStep(ListValue(lb.result()))
}
if (steps.isEmpty) PureStep(ListValue(Nil))
else {
reduceStep(steps.head, currentField, arguments, PathValue.Index(0) :: path) match {
// In 99.99% of the cases, if the head is pure, all the other elements will be pure as well but we catch that error just in case
// NOTE: Our entire test suite passes without catching the error
case step: PureStep =>
try reduceToPureStep(step, steps.tail)
catch { case _: ClassCastException => reduceToListStep(step, steps.tail) }
case step => reduceToListStep(step, steps.tail)
}
}
}
def reduceQuery(q: ZQuery[R, Throwable, Step[R]]): ReducedStep[R] = {
def success(v: Step[R]) = reduceStep(v, currentField, arguments, path)
def fail(e: Cause[Throwable]) = effectfulExecutionError(path, Some(currentField.locationInfo), e)
q.asExitOrElse(null) match {
case null => ReducedStep.QueryStep(q.mapBothCause(fail, success))
case res => res.foldExit(e => ReducedStep.FailureStep(fail(e)), success)
}
}
def reduceStream(stream: ZStream[R, Throwable, Step[R]]): ReducedStep[R] =
if (isSubscription) {
ReducedStep.StreamStep(
stream
.mapErrorCause(effectfulExecutionError(path, Some(currentField.locationInfo), _))
.map(reduceStep(_, currentField, arguments, path))
)
} else {
currentField match {
case IsStream(label, Some(initialCount)) if initialCount > 0 && Feature.isStreamEnabled(flags) =>
ReducedStep.QueryStep(
ZQuery.fromZIONow(
for {
scope <- Scope.make
initialAndRemaining <-
scope.extend[R](
stream
.mapErrorCause(effectfulExecutionError(path, Some(currentField.locationInfo), _))
.peel(ZSink.take[Step[R]](initialCount))
)
} yield {
val (initial, remaining) = initialAndRemaining
ReducedStep.DeferStreamStep(
reduceListStep(initial.toList),
ReducedStep.StreamStep(
ZStream.acquireReleaseExitWith(ZIO.unit)((_, exit) => scope.close(exit)) *>
remaining.map(reduceStep(_, currentField, arguments, path))
),
label,
path,
initialCount
)
}
)
)
case IsStream(label, _) if Feature.isStreamEnabled(flags) =>
ReducedStep.DeferStreamStep(
reduceStep(PureStep(ListValue(Nil)), currentField, arguments, path),
ReducedStep.StreamStep(
stream
.mapErrorCause(effectfulExecutionError(path, Some(currentField.locationInfo), _))
.map(reduceStep(_, currentField, arguments, path))
),
label,
path,
0
)
case _ =>
reduceStep(
QueryStep(ZQuery.fromZIONow(stream.runCollect.map(chunk => ListStep(chunk.toList)))),
currentField,
arguments,
path
)
}
}
def wrapFn[A](step: A => Step[R], input: A): Step[R] =
try step(input)
catch { case NonFatal(e) => Step.fail(e) }
step match {
case s: PureStep => s
case s: QueryStep[R] => reduceQuery(s.query)
case s: ObjectStep[R] => val t = transformer(s, currentField); reduceObjectStep(t.name, t.fields)
case s: FunctionStep[R] => reduceStep(wrapFn(s.step, arguments), currentField, Map.empty, path)
case s: MetadataFunctionStep[R] => reduceStep(wrapFn(s.step, currentField), currentField, arguments, path)
case s: ListStep[R] => reduceListStep(s.steps)
case s: StreamStep[R] => reduceStream(s.inner)
}
}
private def mergeFields(field: Field, typeName: String): List[Field] = {
def matchesTypename(f: Field): Boolean =
f._condition.isEmpty || f._condition.get.contains(typeName)
def mergeFields(fields: List[Field]) = {
val map = new java.util.LinkedHashMap[String, Field](calculateMapCapacity(fields.size))
val nil = Nil
var remaining = fields
while (remaining ne nil) {
val h = remaining.head
if (matchesTypename(h)) {
map.compute(
h.aliasedName,
(_, f) =>
if (f eq null) h
else f.copy(fields = f.fields ::: h.fields)
)
}
remaining = remaining.tail
}
map.values().asScala.toList
}
val fields = field.fields
if (field.allFieldsUniqueNameAndCondition) {
if (fields.isEmpty || !matchesTypename(fields.head)) Nil
else fields
} else mergeFields(fields)
}
/**
* The behaviour of mutable Maps (both Java and Scala) is to resize once the number of entries exceeds
* the capacity * loadFactor (default of 0.75d) threshold in order to prevent hash collisions.
*
* This method is a helper method to estimate the initial map size depending on the number of elements the Map is
* expected to hold
*
* NOTE: This method is the same as java.util.HashMap.calculateHashMapCapacity on JDK19+
*/
private def calculateMapCapacity(nMappings: Int): Int =
Math.ceil(nMappings / 0.75d).toInt
private def fieldInfo(
field: Field,
aliasedName: String,
path: List[PathValue],
fieldDirectives: List[Directive]
): FieldInfo =
FieldInfo(aliasedName, field, path, fieldDirectives, field.parentType)
private def reduceObject(items: List[(String, ReducedStep[R], FieldInfo)]): ReducedStep[R] = {
var hasPures = false
var hasQueries = false
val nil = Nil
var remaining = items
while ((remaining ne nil) && !(hasPures && hasQueries)) {
val isPure = remaining.head._2.isPure
if (isPure && !hasPures) hasPures = true
else if (!isPure && !hasQueries) hasQueries = true
else ()
remaining = remaining.tail
}
if (hasQueries || wrapPureValues) ReducedStep.ObjectStep(items, hasPures, !hasQueries)
else
PureStep(
ObjectValue(items.asInstanceOf[List[(String, PureStep, FieldInfo)]].map { case (k, v, _) => (k, v.value) })
)
}
}
private final class ReducedStepExecutor[R](
queryExecution: QueryExecution,
isMutation: Boolean,
fieldWrappers: List[FieldWrapper[R]],
wrapPureValues: Boolean
)(implicit trace: Trace) {
private type ExecutionQuery[+A] = ZQuery[R, ExecutionError, A]
private val queryExecutionTag = queryExecution.tag
private def collectAll[E, A, B, Coll[+V] <: Iterable[V]](
in: Coll[A],
isTopLevelField: Boolean
)(
as: A => ZQuery[R, E, B]
)(implicit bf: BuildFrom[Coll[A], B, Coll[B]]): ZQuery[R, E, Coll[B]] =
if (in.sizeCompare(1) == 0) as(in.head).map(v => bf.fromSpecific(in)(v :: Nil))
else if (isMutation && isTopLevelField) ZQuery.foreach(in)(as)
else
(queryExecutionTag: @switch) match {
case QueryExecution.Sequential.tag => ZQuery.foreach(in)(as)
case QueryExecution.Parallel.tag => ZQuery.foreachPar(in)(as)
case QueryExecution.Batched.tag => ZQuery.foreachBatched(in)(as)
case QueryExecution.Mixed.tag =>
if (isTopLevelField) ZQuery.foreachPar(in)(as) else ZQuery.foreachBatched(in)(as)
}
def makeQuery(
step: ReducedStep[R],
errors: AtomicReference[List[CalibanError]],
deferred: AtomicReference[List[Deferred[R]]]
): URQuery[R, ResponseValue] = {
def handleError(error: ExecutionError): ResponseValue = {
errors.updateAndGet(error :: _)
NullValue
}
def wrap(query: ExecutionQuery[ResponseValue], isPure: Boolean, fieldInfo: FieldInfo) = {
@tailrec
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)
}
if ((isPure && !wrapPureValues) || (fieldWrappers eq Nil)) query
else {
loop(query, fieldWrappers).mapErrorCause(e =>
effectfulExecutionError(
PathValue.Key(fieldInfo.name) :: fieldInfo.path,
Some(fieldInfo.details.locationInfo),
e
)
)
}
}
def objectFieldQuery(step: ReducedStep[R], info: FieldInfo, isPure: Boolean = false) = {
val q = wrap(loop(step), isPure, info)
if (info.details.fieldType.isNullable) q.fold(handleError, identity) else q
}
def makeObjectQuery(
steps: List[(String, ReducedStep[R], FieldInfo)],
hasPureFields: Boolean,
isTopLevelField: Boolean
) = {
def collectAllQueries() = {
def combineQueryResults(results: List[ResponseValue]) = {
val builder = ListBuffer.empty[(String, ResponseValue)]
val nil = Nil
var resps = results
var names = steps
while (resps ne nil) {
val (name, _, _) = names.head
builder addOne ((name, resps.head))
resps = resps.tail
names = names.tail
}
ObjectValue(builder.result())
}
steps match {
case (name, step, info) :: Nil =>
// Shortcut for single field queries
objectFieldQuery(step, info, wrapPureValues && step.isPure).map(v => ObjectValue((name, v) :: Nil))
case steps =>
collectAll(steps, isTopLevelField) { case (_, step, info) =>
// Only way we could have ended with pure fields here is if we wrap pure values, so we check that first as it's cheaper
objectFieldQuery(step, info, wrapPureValues && step.isPure)
}.map(combineQueryResults)
}
}
def combineResults(names: List[String], resolved: List[ResponseValue])(fromQueries: List[ResponseValue]) = {
val nil = Nil
var results: List[(String, ResponseValue)] = nil
var remainingQueries = fromQueries
var remainingResponses = resolved
var remainingNames = names
while (remainingResponses ne nil) {
val name = remainingNames.head
val resp = remainingResponses.head match {
case null =>
val v = remainingQueries.head
remainingQueries = remainingQueries.tail
v
case v => v
}
results = (name, resp) :: results
remainingResponses = remainingResponses.tail
remainingNames = remainingNames.tail
}
ObjectValue(results)
}
def collectMixed() = {
val nil = Nil // Bring into stack memory to avoid fetching from heap on each iteration
var queries: List[(String, ReducedStep[R], FieldInfo)] = nil
var names: List[String] = nil
var resolved: List[ResponseValue] = nil
var remaining = steps
while (remaining ne nil) {
val t @ (name, step, _) = remaining.head
val value = step match {
case PureStep(value) => value
case _ =>
queries = t :: queries
null
}
resolved = value :: resolved
names = name :: names
remaining = remaining.tail
}
collectAll(queries, isTopLevelField) { case (_, s, i) => objectFieldQuery(s, i) }
.map(combineResults(names, resolved))
}
if (hasPureFields && !wrapPureValues) collectMixed() else collectAllQueries()
}
def makeListQuery(steps: List[ReducedStep[R]], areItemsNullable: Boolean): ExecutionQuery[ResponseValue] =
collectAll(steps, isTopLevelField = false)(
if (areItemsNullable) loop(_).fold(handleError, identity)
else loop(_)
)
.map(ListValue.apply)
def loop(step: ReducedStep[R], isTopLevelField: Boolean = false): ExecutionQuery[ResponseValue] =
step match {
case PureStep(value) => ZQuery.succeedNow(value)
case ReducedStep.QueryStep(step) => step.flatMap(loop(_))
case ReducedStep.ObjectStep(steps, hasPureFields, _) => makeObjectQuery(steps, hasPureFields, isTopLevelField)
case ReducedStep.ListStep(steps, areItemsNullable, _) => makeListQuery(steps, areItemsNullable)
case ReducedStep.StreamStep(stream) =>
ZQuery
.environmentWith[R](env =>
ResponseValue.StreamValue(
stream.mapChunksZIO { chunk =>
collectAll(chunk, isTopLevelField)(loop(_).catchAllZIO(_ => nullValueExit)).run
}.provideEnvironment(env)
)
)
case ReducedStep.DeferStep(obj, nextSteps, path) =>
val deferredSteps = nextSteps.map { case (step, label) =>
DeferredFragment(path, step, label)
}
deferred.updateAndGet(deferredSteps ::: _)
loop(obj)
case ReducedStep.DeferStreamStep(initial, remaining, label, path, startFrom) =>
val streamStep = DeferredStream(path, remaining, label, startFrom)
deferred.updateAndGet(streamStep :: _)
loop(initial)
}
loop(step, isTopLevelField = true).fold(handleError, identity)
}
}
private def effectfulExecutionError(
path: List[PathValue],
locationInfo: Option[LocationInfo],
cause: Cause[Throwable]
): Cause[ExecutionError] =
cause.failureOption orElse cause.defects.headOption match {
case Some(e: ExecutionError) if e.path.isEmpty =>
Cause.fail(e.copy(path = path.reverse, locationInfo = locationInfo))
case Some(e: ExecutionError) => Cause.fail(e)
case other => Cause.fail(ExecutionError("Effect failure", path.reverse, locationInfo, other))
}
private val nullValueExit = Exit.succeed(NullValue)
// The implicit classes below are for methods that don't exist in Scala 2.12 so we add them as syntax methods instead
private implicit class EnrichedListOps[+A](private val list: List[A]) extends AnyVal {
def partitionMap[A1, A2](f: A => Either[A1, A2]): (List[A1], List[A2]) = {
val l = List.newBuilder[A1]
val r = List.newBuilder[A2]
list.foreach { x =>
f(x) match {
case Left(x1) => l += x1
case Right(x2) => r += x2
}
}
(l.result(), r.result())
}
}
private implicit class EnrichedListBufferOps[A](private val lb: ListBuffer[A]) extends AnyVal {
def mapInPlace[B](f: A => B): ListBuffer[B] = lb.map(f)
def addOne(value: A): ListBuffer[A] = lb += value
}
private implicit class EnrichedVectorBuilderOps[A](private val lb: VectorBuilder[A]) extends AnyVal {
def addOne(value: A): VectorBuilder[A] = lb += value
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy