diff --git a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala index fa5d277eca..d711a9020e 100644 --- a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala @@ -85,6 +85,8 @@ trait CommonSchemaDerivation[R] { Some(ctx.typeName.full) ) + override private[schema] def resolveFieldLazily: Boolean = !ctx.isObject + override def resolve(value: T): Step[R] = if (ctx.isObject) PureStep(EnumValue(getName(ctx))) else if ((ctx.isValueClass || isValueType(ctx)) && ctx.parameters.nonEmpty) { @@ -92,7 +94,13 @@ trait CommonSchemaDerivation[R] { head.typeclass.resolve(head.dereference(value)) } else { val fields = Map.newBuilder[String, Step[R]] - ctx.parameters.foreach(p => fields += getName(p) -> p.typeclass.resolve(p.dereference(value))) + ctx.parameters.foreach(p => + fields += getName(p) -> { + lazy val step = p.typeclass.resolve(p.dereference(value)) + if (p.typeclass.resolveFieldLazily) FunctionStep(_ => step) + else step + } + ) ObjectStep(getName(ctx), fields.result()) } } diff --git a/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala index 799986c864..935f8f7fbb 100644 --- a/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala @@ -4,7 +4,7 @@ import caliban.Value.EnumValue import caliban.introspection.adt.* import caliban.parsing.adt.Directive import caliban.schema.Annotations.* -import caliban.schema.Step.ObjectStep +import caliban.schema.Step.{ FunctionStep, ObjectStep } import caliban.schema.Types.* import caliban.schema.macros.{ Macros, TypeInfo } @@ -154,6 +154,8 @@ trait CommonSchemaDerivation { else if (isInput) mkInputObject[R](annotations, fields, info, paramAnnotations)(isInput, isSubscription) else mkObject[R](annotations, fields, info, paramAnnotations)(isInput, isSubscription) + override private[schema] lazy val resolveFieldLazily: Boolean = fields.nonEmpty + def resolve(value: A): Step[R] = if (fields.isEmpty) PureStep(EnumValue(name)) else if (isValueType) { @@ -163,9 +165,11 @@ trait CommonSchemaDerivation { val fieldsBuilder = Map.newBuilder[String, Step[R]] fields.foreach { case (label, _, schema, index) => val fieldAnnotations = paramAnnotations.getOrElse(label, Nil) - fieldsBuilder += getName(fieldAnnotations, label) -> schema.resolve( - value.asInstanceOf[Product].productElement(index) - ) + lazy val step = schema.resolve(value.asInstanceOf[Product].productElement(index)) + fieldsBuilder += getName(fieldAnnotations, label) -> { + if (schema.resolveFieldLazily) FunctionStep(_ => step) + else step + } } ObjectStep(name, fieldsBuilder.result()) } diff --git a/core/src/main/scala/caliban/schema/Schema.scala b/core/src/main/scala/caliban/schema/Schema.scala index ffda14506f..723586c56b 100644 --- a/core/src/main/scala/caliban/schema/Schema.scala +++ b/core/src/main/scala/caliban/schema/Schema.scala @@ -81,6 +81,15 @@ trait Schema[-R, T] { self => */ def arguments: List[__InputValue] = Nil + /** + * Boolean flag indicating whether the [[resolve]] method should be wrapped in a [[FunctionStep]] when resolving + * object fields. + * + * Should be `false` except for objects or wrappers of objects. This prevents fields of case classes to be resolved when they're not + * requested in the query, which significantly improves performance in larger schemas + */ + private[schema] def resolveFieldLazily: Boolean = false + /** * Builds a new `Schema` of `A` from an existing `Schema` of `T` and a function from `A` to `T`. * @param f a function from `A` to `T`. @@ -90,6 +99,7 @@ trait Schema[-R, T] { self => override def arguments: List[__InputValue] = self.arguments override def toType(isInput: Boolean, isSubscription: Boolean): __Type = self.toType_(isInput, isSubscription) override def resolve(value: A): Step[R] = self.resolve(f(value)) + override private[schema] def resolveFieldLazily: Boolean = self.resolveFieldLazily } /** @@ -110,7 +120,8 @@ trait Schema[-R, T] { self => case _ => true } - override def resolve(value: T): Step[R] = + override private[schema] def resolveFieldLazily: Boolean = self.resolveFieldLazily + override def resolve(value: T): Step[R] = self.resolve(value) match { case o @ ObjectStep(_, fields) => if (renameTypename) ObjectStep(name, fields) else o @@ -174,7 +185,8 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { private lazy val fieldsForResolve = fields(false, false) - override def resolve(value: A): Step[R1] = { + override private[schema] def resolveFieldLazily: Boolean = true + override def resolve(value: A): Step[R1] = { val fieldsBuilder = Map.newBuilder[String, Step[R1]] fieldsForResolve.foreach { case (f, plan) => fieldsBuilder += f.name -> plan(value) } ObjectStep(name, fieldsBuilder.result()) @@ -282,7 +294,8 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { override def optional: Boolean = true override def toType(isInput: Boolean, isSubscription: Boolean): __Type = ev.toType_(isInput, isSubscription) - override def resolve(value: Option[A]): Step[R0] = + override private[schema] def resolveFieldLazily: Boolean = ev.resolveFieldLazily + override def resolve(value: Option[A]): Step[R0] = value match { case Some(value) => ev.resolve(value) case None => NullStep @@ -294,7 +307,8 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { (if (ev.optional) t else t.nonNull).list } - override def resolve(value: List[A]): Step[R0] = ListStep(value.map(ev.resolve)) + override private[schema] def resolveFieldLazily: Boolean = ev.resolveFieldLazily + override def resolve(value: List[A]): Step[R0] = ListStep(value.map(ev.resolve)) } implicit def setSchema[R0, A](implicit ev: Schema[R0, A]): Schema[R0, Set[A]] = listSchema[R0, A].contramap(_.toList) implicit def seqSchema[R0, A](implicit ev: Schema[R0, A]): Schema[R0, Seq[A]] = listSchema[R0, A].contramap(_.toList) @@ -379,6 +393,7 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { override def toType(isInput: Boolean, isSubscription: Boolean): __Type = kvSchema.toType_(isInput, isSubscription).nonNull.list + override private[schema] def resolveFieldLazily: Boolean = evA.resolveFieldLazily || evB.resolveFieldLazily override def resolve(value: Map[A, B]): Step[RA with RB] = ListStep(value.toList.map(kvSchema.resolve)) } implicit def functionSchema[RA, RB, A, B](implicit