From 2875dd0b234d48ce58233686f503f46562ec851a Mon Sep 17 00:00:00 2001 From: Adam Fraser Date: Fri, 10 Jan 2020 19:14:53 -0800 Subject: [PATCH] Improvements to ZQuery (#160) * ZQuery improvements * don't use environment for DataSource * add additional error handling combinators * use bimap --- .../scala/caliban/execution/Executor.scala | 15 +- .../main/scala/zquery/BlockedRequestMap.scala | 14 +- core/src/main/scala/zquery/Cache.scala | 10 +- .../scala/zquery/CompletedRequestMap.scala | 6 +- core/src/main/scala/zquery/DataSource.scala | 324 ++++++++++-------- .../scala/zquery/DataSourceFunction.scala | 16 +- core/src/main/scala/zquery/Described.scala | 17 + core/src/main/scala/zquery/QueryFailure.scala | 4 +- core/src/main/scala/zquery/Result.scala | 22 +- core/src/main/scala/zquery/ZQuery.scala | 177 ++++++---- core/src/test/scala/zquery/ZQuerySpec.scala | 7 +- .../caliban/optimizations/OptimizedTest.scala | 30 +- vuepress/docs/docs/optimization.md | 8 +- 13 files changed, 373 insertions(+), 277 deletions(-) create mode 100644 core/src/main/scala/zquery/Described.scala diff --git a/core/src/main/scala/caliban/execution/Executor.scala b/core/src/main/scala/caliban/execution/Executor.scala index ceb230984..398e85415 100644 --- a/core/src/main/scala/caliban/execution/Executor.scala +++ b/core/src/main/scala/caliban/execution/Executor.scala @@ -118,9 +118,10 @@ object Executor { reduceObject(items) case QueryStep(inner) => ReducedStep.QueryStep( - inner - .map(reduceStep(_, currentField, arguments)) - .mapError(GenericSchema.effectfulExecutionError(currentField.name, _)) + inner.bimap( + GenericSchema.effectfulExecutionError(currentField.name, _), + reduceStep(_, currentField, arguments) + ) ) case StreamStep(stream) => ReducedStep.StreamStep( @@ -142,10 +143,10 @@ object Executor { val queries = steps.map { case (name, field) => loop(field).map(name -> _) } (if (allowParallelism) ZQuery.collectAllPar(queries) else ZQuery.collectAll(queries)).map(ObjectValue) case ReducedStep.QueryStep(step) => - step.fold(Left(_), Right(_)).flatMap { - case Left(error) => ZQuery.fromEffect(errors.update(error :: _)).map(_ => NullValue) - case Right(query) => loop(query) - } + step.foldM( + error => ZQuery.fromEffect(errors.update(error :: _)).map(_ => NullValue), + query => loop(query) + ) case ReducedStep.StreamStep(stream) => ZQuery .fromEffect(ZIO.environment[R]) diff --git a/core/src/main/scala/zquery/BlockedRequestMap.scala b/core/src/main/scala/zquery/BlockedRequestMap.scala index 699adca5b..6a68a76a9 100644 --- a/core/src/main/scala/zquery/BlockedRequestMap.scala +++ b/core/src/main/scala/zquery/BlockedRequestMap.scala @@ -7,13 +7,13 @@ import zio.ZIO * requests from those data sources. */ private[zquery] final class BlockedRequestMap[-R]( - private val map: Map[DataSource.Service[Any, Any], Vector[BlockedRequest[Any]]] + private val map: Map[DataSource[Any, Any], Vector[BlockedRequest[Any]]] ) { self => def ++[R1 <: R](that: BlockedRequestMap[R1]): BlockedRequestMap[R1] = new BlockedRequestMap( (self.map.toVector ++ that.map.toVector) - .foldLeft[Map[DataSource.Service[Any, Any], Vector[BlockedRequest[Any]]]](Map()) { + .foldLeft[Map[DataSource[Any, Any], Vector[BlockedRequest[Any]]]](Map()) { case (acc, (key, value)) => acc + (key -> acc.get(key).fold(value)(_ ++ value)) } @@ -24,8 +24,8 @@ private[zquery] final class BlockedRequestMap[-R]( * can change the environment and error types of data sources but must * preserve the request type of each data source. */ - final def mapDataSources[R1](f: DataSourceFunction[R, R1]): BlockedRequestMap[R1] = - new BlockedRequestMap(self.map.map { case (k, v) => (f(k).asInstanceOf[DataSource.Service[Any, Any]], v) }) + def mapDataSources[R1](f: DataSourceFunction[R, R1]): BlockedRequestMap[R1] = + new BlockedRequestMap(self.map.map { case (k, v) => (f(k).asInstanceOf[DataSource[Any, Any]], v) }) /** * Executes all requests, submitting batched requests to each data source in @@ -50,11 +50,11 @@ object BlockedRequestMap { * specified data source to the specified request. */ def apply[R, E, K]( - dataSource: DataSource.Service[R, K], + dataSource: DataSource[R, K], blockedRequest: BlockedRequest[K] ): BlockedRequestMap[R] = new BlockedRequestMap( - Map(dataSource.asInstanceOf[DataSource.Service[Any, Any]] -> Vector(blockedRequest)) + Map(dataSource.asInstanceOf[DataSource[Any, Any]] -> Vector(blockedRequest)) ) /** @@ -62,6 +62,6 @@ object BlockedRequestMap { */ val empty: BlockedRequestMap[Any] = new BlockedRequestMap( - Map.empty[DataSource.Service[Any, Any], Vector[BlockedRequest[Any]]] + Map.empty[DataSource[Any, Any], Vector[BlockedRequest[Any]]] ) } diff --git a/core/src/main/scala/zquery/Cache.scala b/core/src/main/scala/zquery/Cache.scala index 87479d15f..648901375 100644 --- a/core/src/main/scala/zquery/Cache.scala +++ b/core/src/main/scala/zquery/Cache.scala @@ -1,6 +1,6 @@ package zquery -import zio.{ Ref, UIO } +import zio.{ IO, Ref, UIO } /** * A `Cache` maintains an internal state with a mapping from requests to `Ref`s @@ -8,13 +8,13 @@ import zio.{ Ref, UIO } * is used internally by the library to provide deduplication and caching of * requests. */ -class Cache private (private val state: Ref[Map[Any, Any]]) { +final class Cache private (private val state: Ref[Map[Any, Any]]) { /** * Inserts a request and a `Ref` that will contain the result of the request * when it is executed into the cache. */ - final def insert[E, A](request: Request[E, A], result: Ref[Option[Either[E, A]]]): UIO[Unit] = + def insert[E, A](request: Request[E, A], result: Ref[Option[Either[E, A]]]): UIO[Unit] = state.update(_ + (request -> result)).unit /** @@ -23,8 +23,8 @@ class Cache private (private val state: Ref[Map[Any, Any]]) { * been executed yet, or `Some(Ref(Some(value)))` if the request has been * executed. */ - final def lookup[E, A](request: Request[E, A]): UIO[Option[Ref[Option[Either[E, A]]]]] = - state.get.map(_.get(request).asInstanceOf[Option[Ref[Option[Either[E, A]]]]]) + def lookup[E, A](request: Request[E, A]): IO[Unit, Ref[Option[Either[E, A]]]] = + state.get.map(_.get(request).asInstanceOf[Option[Ref[Option[Either[E, A]]]]]).get } object Cache { diff --git a/core/src/main/scala/zquery/CompletedRequestMap.scala b/core/src/main/scala/zquery/CompletedRequestMap.scala index 9e9d94cbb..fec90011f 100644 --- a/core/src/main/scala/zquery/CompletedRequestMap.scala +++ b/core/src/main/scala/zquery/CompletedRequestMap.scala @@ -11,19 +11,19 @@ package zquery */ final class CompletedRequestMap private (private val map: Map[Any, Either[Any, Any]]) { self => - final def ++(that: CompletedRequestMap): CompletedRequestMap = + def ++(that: CompletedRequestMap): CompletedRequestMap = new CompletedRequestMap(self.map ++ that.map) /** * Appends the specified result to the completed requests map. */ - final def insert[E, A](request: Request[E, A])(result: Either[E, A]): CompletedRequestMap = + def insert[E, A](request: Request[E, A])(result: Either[E, A]): CompletedRequestMap = new CompletedRequestMap(self.map + (request -> result)) /** * Retrieves the result of the specified request if it exists. */ - final def lookup[E, A](request: Request[E, A]): Option[Either[E, A]] = + def lookup[E, A](request: Request[E, A]): Option[Either[E, A]] = map.get(request).asInstanceOf[Option[Either[E, A]]] } diff --git a/core/src/main/scala/zquery/DataSource.scala b/core/src/main/scala/zquery/DataSource.scala index 09da29da3..289c0de57 100644 --- a/core/src/main/scala/zquery/DataSource.scala +++ b/core/src/main/scala/zquery/DataSource.scala @@ -1,6 +1,6 @@ package zquery -import zio.ZIO +import zio.{ NeedsEnv, ZIO } /** * A `DataSource[R, A]` is capable of executing requests of type `A` that @@ -22,163 +22,191 @@ import zio.ZIO * sources must provide requests for all results received or fail with an `E`. * Failure to do so will cause a query to die with a `QueryFailure` when run. */ -trait DataSource[-R, -A] { - def dataSource: DataSource.Service[R, A] -} - -object DataSource { - - trait Service[-R, -A] { self => - - /** - * The data source's identifier. - */ - val identifier: String - - /** - * Execute a collection of requests. Data sources are guaranteed that the - * collection will contain at least one request and that all requests will - * be unique when they are called by `ZQuery`. - */ - def run(requests: Iterable[A]): ZIO[R, Nothing, CompletedRequestMap] - - /** - * Returns a new data source that executes requests of type `B` using the - * specified function to transform `B` requests into requests that this - * data source can execute. - */ - final def contramap[B](f: B => A)(name: String): DataSource.Service[R, B] = - new DataSource.Service[R, B] { - val identifier = s"${self.identifier}.contramap($name)" - def run(requests: Iterable[B]): ZIO[R, Nothing, CompletedRequestMap] = - self.run(requests.map(f)) - } +trait DataSource[-R, -A] { self => + + /** + * The data source's identifier. + */ + val identifier: String + + /** + * Execute a collection of requests. Data sources are guaranteed that the + * collection will contain at least one request and that all requests will be + * unique when they are called by `ZQuery`. + */ + def run(requests: Iterable[A]): ZIO[R, Nothing, CompletedRequestMap] + + /** + * Returns a new data source that executes requests of type `B` using the + * specified function to transform `B` requests into requests that this data + * source can execute. + */ + final def contramap[B](f: Described[B => A]): DataSource[R, B] = + new DataSource[R, B] { + val identifier = s"${self.identifier}.contramap(${f.description})" + def run(requests: Iterable[B]): ZIO[R, Nothing, CompletedRequestMap] = + self.run(requests.map(f.value)) + } - /** - * Returns a new data source that executes requests of type `B` using the - * specified effectual function to transform `B` requests into requests - * that this data source can execute. - */ - final def contramapM[R1 <: R, B](name: String)(f: B => ZIO[R1, Nothing, A]): DataSource.Service[R1, B] = - new DataSource.Service[R1, B] { - val identifier = s"${self.identifier}.contramapM($name)" - def run(requests: Iterable[B]): ZIO[R1, Nothing, CompletedRequestMap] = - ZIO.foreach(requests)(f).flatMap(self.run) - } + /** + * Returns a new data source that executes requests of type `B` using the + * specified effectual function to transform `B` requests into requests that + * this data source can execute. + */ + final def contramapM[R1 <: R, B](f: Described[B => ZIO[R1, Nothing, A]]): DataSource[R1, B] = + new DataSource[R1, B] { + val identifier = s"${self.identifier}.contramapM(${f.description})" + def run(requests: Iterable[B]): ZIO[R1, Nothing, CompletedRequestMap] = + ZIO.foreach(requests)(f.value).flatMap(self.run) + } - /** - * Returns a new data source that executes requests of type `C` using the - * specified function to transform `C` requests into requests that either - * this data source or that data source can execute. - */ - final def eitherWith[R1 <: R, B, C]( - that: DataSource.Service[R1, B] - )(name: String)(f: C => Either[A, B]): DataSource.Service[R1, C] = - new DataSource.Service[R1, C] { - val identifier = s"${self.identifier}.eitherWith(${that.identifier})($name)" - def run(requests: Iterable[C]): ZIO[R1, Nothing, CompletedRequestMap] = { - val (as, bs) = requests.foldLeft((List.empty[A], List.empty[B])) { - case ((as, bs), c) => - f(c) match { - case Left(a) => (a :: as, bs) - case Right(b) => (as, b :: bs) - } - } - self.run(as).zipWithPar(that.run(bs))(_ ++ _) + /** + * Returns a new data source that executes requests of type `C` using the + * specified function to transform `C` requests into requests that either + * this data source or that data source can execute. + */ + final def eitherWith[R1 <: R, B, C]( + that: DataSource[R1, B] + )(f: Described[C => Either[A, B]]): DataSource[R1, C] = + new DataSource[R1, C] { + val identifier = s"${self.identifier}.eitherWith(${that.identifier})(${f.description})" + def run(requests: Iterable[C]): ZIO[R1, Nothing, CompletedRequestMap] = { + val (as, bs) = requests.foldLeft((List.empty[A], List.empty[B])) { + case ((as, bs), c) => + f.value(c) match { + case Left(a) => (a :: as, bs) + case Right(b) => (as, b :: bs) + } } + self.run(as).zipWithPar(that.run(bs))(_ ++ _) } + } + + override final def equals(that: Any): Boolean = that match { + case that: DataSource[_, _] => this.identifier == that.identifier + } - override final def equals(that: Any): Boolean = that match { - case that: DataSource.Service[_, _] => this.identifier == that.identifier + override final def hashCode: Int = + identifier.hashCode + + /** + * Provides this data source with its required environment. + */ + final def provide(r: Described[R])(implicit ev: NeedsEnv[R]): DataSource[Any, A] = + provideSome(Described(_ => r.value, s"_ => ${r.description}")) + + /** + * Provides this data source with part of its required environment. + */ + final def provideSome[R0](f: Described[R0 => R])(implicit ev: NeedsEnv[R]): DataSource[R0, A] = + new DataSource[R0, A] { + val identifier = s"${self.identifier}.provideSome(${f.description})" + def run(requests: Iterable[A]): ZIO[R0, Nothing, CompletedRequestMap] = + self.run(requests).provideSome(f.value) } - override final def hashCode: Int = - identifier.hashCode - - /** - * Provides this data source with its required environment. - */ - final def provide(name: String)(r: R): DataSource.Service[Any, A] = - provideSome(s"_ => $name")(_ => r) - - /** - * Provides this data source with part of its required environment. - */ - final def provideSome[R0](name: String)(f: R0 => R): DataSource.Service[R0, A] = - new DataSource.Service[R0, A] { - val identifier = s"${self.identifier}.provideSome($name)" - def run(requests: Iterable[A]): ZIO[R0, Nothing, CompletedRequestMap] = - self.run(requests).provideSome(f) - } + override final def toString: String = + identifier +} - override final def toString: String = - identifier - } +object DataSource { - object Service { - - /** - * Constructs a data source from a function taking a collection of - * requests and returning a `CompletedRequestMap`. - */ - def apply[R, A](name: String)(f: Iterable[A] => ZIO[R, Nothing, CompletedRequestMap]): DataSource.Service[R, A] = - new DataSource.Service[R, A] { - val identifier = name - def run(requests: Iterable[A]): ZIO[R, Nothing, CompletedRequestMap] = - f(requests) - } + /** + * Constructs a data source from a function taking a collection of requests + * and returning a `CompletedRequestMap`. + */ + def apply[R, A](name: String)(f: Iterable[A] => ZIO[R, Nothing, CompletedRequestMap]): DataSource[R, A] = + new DataSource[R, A] { + val identifier = name + def run(requests: Iterable[A]): ZIO[R, Nothing, CompletedRequestMap] = + f(requests) + } - /** - * Constructs a data source from a pure function. - */ - final def fromFunction[A, B]( - name: String - )(f: A => B)(implicit ev: A <:< Request[Nothing, B]): DataSource.Service[Any, A] = - new DataSource.Service[Any, A] { - val identifier = name - def run(requests: Iterable[A]): ZIO[Any, Nothing, CompletedRequestMap] = - ZIO.succeed(requests.foldLeft(CompletedRequestMap.empty)((map, k) => map.insert(k)(Right(f(k))))) - } + /** + * Constructs a data source from a pure function. + */ + def fromFunction[A, B]( + name: String + )(f: A => B)(implicit ev: A <:< Request[Nothing, B]): DataSource[Any, A] = + new DataSource[Any, A] { + val identifier = name + def run(requests: Iterable[A]): ZIO[Any, Nothing, CompletedRequestMap] = + ZIO.succeed(requests.foldLeft(CompletedRequestMap.empty)((map, k) => map.insert(k)(Right(f(k))))) + } - /** - * Constructs a data source from a pure function that takes a list of requests and returns a list of results of the same size. - * Each item in the result list must correspond to the item at the same index in the request list. - */ - final def fromFunctionBatched[A, B]( - name: String - )(f: Iterable[A] => Iterable[B])(implicit ev: A <:< Request[Nothing, B]): DataSource.Service[Any, A] = - fromFunctionBatchedM(name)(f andThen ZIO.succeed) - - /** - * Constructs a data source from an effectual function that takes a list of requests and returns a list of results of the same size. - * Each item in the result list must correspond to the item at the same index in the request list. - */ - final def fromFunctionBatchedM[R, E, A, B]( - name: String - )(f: Iterable[A] => ZIO[R, Nothing, Iterable[B]])(implicit ev: A <:< Request[E, B]): DataSource.Service[R, A] = - new DataSource.Service[R, A] { - val identifier = name - def run(requests: Iterable[A]): ZIO[R, Nothing, CompletedRequestMap] = - f(requests).map( - results => - (requests zip results).foldLeft(CompletedRequestMap.empty) { - case (map, (k, v)) => map.insert(k)(Right(v)) - } - ) - } + /** + * Constructs a data source from a pure function that takes a list of + * requests and returns a list of results of the same size. Each item in the + * result list must correspond to the item at the same index in the request + * list. + */ + def fromFunctionBatched[A, B]( + name: String + )(f: Iterable[A] => Iterable[B])(implicit ev: A <:< Request[Nothing, B]): DataSource[Any, A] = + fromFunctionBatchedM(name)(f andThen ZIO.succeed) + + /** + * Constructs a data source from an effectual function that takes a list of + * requests and returns a list of results of the same size. Each item in the + * result list must correspond to the item at the same index in the request + * list. + */ + def fromFunctionBatchedM[R, E, A, B]( + name: String + )(f: Iterable[A] => ZIO[R, Nothing, Iterable[B]])(implicit ev: A <:< Request[E, B]): DataSource[R, A] = + new DataSource[R, A] { + val identifier = name + def run(requests: Iterable[A]): ZIO[R, Nothing, CompletedRequestMap] = + f(requests).map { results => + (requests zip results).foldLeft(CompletedRequestMap.empty) { + case (map, (k, v)) => map.insert(k)(Right(v)) + } + } + } - /** - * Constructs a data source from an effectual function. - */ - final def fromFunctionM[R, E, A, B]( - name: String - )(f: A => ZIO[R, Nothing, B])(implicit ev: A <:< Request[E, B]): DataSource.Service[R, A] = - new DataSource.Service[R, A] { - val identifier = name - def run(requests: Iterable[A]): ZIO[R, Nothing, CompletedRequestMap] = - ZIO - .foreachPar(requests)(k => ZIO.succeed(k).zip(f(k))) - .map(_.foldLeft(CompletedRequestMap.empty) { case (map, (k, v)) => map.insert(k)(Right(v)) }) - } - } + /** + * Constructs a data source from a function that takes a list of requests and + * returns a list of results of the same size. Uses the specified function to + * associate each result with the corresponding effect, allowing the function + * to return the list of results in a different order than the list of + * requests. + */ + def fromFunctionBatchedWith[A, B]( + name: String + )(f: Iterable[A] => Iterable[B], g: B => Request[Nothing, B]): DataSource[Any, A] = + fromFunctionBatchedWithM(name)(f andThen ZIO.succeed, g) + + /** + * Constructs a data source from an effectual function that takes a list of + * requests and returns a list of results of the same size. Uses the + * specified function to associate each result with the corresponding effect, + * allowing the function to return the list of results in a different order + * than the list of requests. + */ + def fromFunctionBatchedWithM[R, E, A, B]( + name: String + )(f: Iterable[A] => ZIO[R, Nothing, Iterable[B]], g: B => Request[E, B]): DataSource[R, A] = + new DataSource[R, A] { + val identifier = name + def run(requests: Iterable[A]): ZIO[R, Nothing, CompletedRequestMap] = + f(requests).map { results => + results.map(b => (g(b), b)).foldLeft(CompletedRequestMap.empty) { + case (map, (k, v)) => map.insert(k)(Right(v)) + } + } + } + + /** + * Constructs a data source from an effectual function. + */ + def fromFunctionM[R, E, A, B]( + name: String + )(f: A => ZIO[R, Nothing, B])(implicit ev: A <:< Request[E, B]): DataSource[R, A] = + new DataSource[R, A] { + val identifier = name + def run(requests: Iterable[A]): ZIO[R, Nothing, CompletedRequestMap] = + ZIO + .foreachPar(requests)(k => ZIO.succeed(k).zip(f(k))) + .map(_.foldLeft(CompletedRequestMap.empty) { case (map, (k, v)) => map.insert(k)(Right(v)) }) + } } diff --git a/core/src/main/scala/zquery/DataSourceFunction.scala b/core/src/main/scala/zquery/DataSourceFunction.scala index b5d60a9bd..6d1d647b6 100644 --- a/core/src/main/scala/zquery/DataSourceFunction.scala +++ b/core/src/main/scala/zquery/DataSourceFunction.scala @@ -9,7 +9,7 @@ package zquery */ trait DataSourceFunction[+R, -R1] { self => - def apply[A](dataSource: DataSource.Service[R, A]): DataSource.Service[R1, A] + def apply[A](dataSource: DataSource[R, A]): DataSource[R1, A] /** * A symbolic alias for `compose`. @@ -29,7 +29,7 @@ trait DataSourceFunction[+R, -R1] { self => */ final def andThen[R2](that: DataSourceFunction[R1, R2]): DataSourceFunction[R, R2] = new DataSourceFunction[R, R2] { - def apply[A](dataSource: DataSource.Service[R, A]): DataSource.Service[R2, A] = + def apply[A](dataSource: DataSource[R, A]): DataSource[R2, A] = that(self(dataSource)) } @@ -39,7 +39,7 @@ trait DataSourceFunction[+R, -R1] { self => */ final def compose[R0](that: DataSourceFunction[R0, R]): DataSourceFunction[R0, R1] = new DataSourceFunction[R0, R1] { - def apply[A](dataSource: DataSource.Service[R0, A]): DataSource.Service[R1, A] = + def apply[A](dataSource: DataSource[R0, A]): DataSource[R1, A] = self(that(dataSource)) } } @@ -50,16 +50,16 @@ object DataSourceFunction { * A data source function that provides a data source with its required * environment. */ - final def provide[R](name: String)(r: R): DataSourceFunction[R, Any] = - provideSome(s"_ => $name")(_ => r) + def provide[R](r: Described[R]): DataSourceFunction[R, Any] = + provideSome(Described(_ => r.value, s"_ => ${r.description}")) /** * A data source function that provides a data sources with part of its * required environment. */ - final def provideSome[R, R1](name: String)(f: R1 => R): DataSourceFunction[R, R1] = + def provideSome[R, R1](f: Described[R1 => R]): DataSourceFunction[R, R1] = new DataSourceFunction[R, R1] { - def apply[A](dataSource: DataSource.Service[R, A]): DataSource.Service[R1, A] = - dataSource.provideSome(name)(f) + def apply[A](dataSource: DataSource[R, A]): DataSource[R1, A] = + dataSource.provideSome(f) } } diff --git a/core/src/main/scala/zquery/Described.scala b/core/src/main/scala/zquery/Described.scala new file mode 100644 index 000000000..4826d9949 --- /dev/null +++ b/core/src/main/scala/zquery/Described.scala @@ -0,0 +1,17 @@ +package zquery + +/** + * A `Described[A]` is a value of type `A` along with a string description of + * that value. The description may be used to generate a hash associated with + * the value, so values that are equal should have the same description and + * values that are not equal should have different descriptions. + */ +final case class Described[+A](value: A, description: String) + +object Described { + + implicit class AnySyntax[A](private val value: A) extends AnyVal { + def ?(description: String): Described[A] = + Described(value, description) + } +} diff --git a/core/src/main/scala/zquery/QueryFailure.scala b/core/src/main/scala/zquery/QueryFailure.scala index e177fb511..760fb1d80 100644 --- a/core/src/main/scala/zquery/QueryFailure.scala +++ b/core/src/main/scala/zquery/QueryFailure.scala @@ -3,8 +3,8 @@ package zquery /** * `QueryFailure` keeps track of details relevant to query failures. */ -final case class QueryFailure(dataSource: DataSource.Service[Nothing, Nothing], request: Request[Any, Any]) +final case class QueryFailure(dataSource: DataSource[Nothing, Nothing], request: Request[Any, Any]) extends Throwable(null, null, true, false) { - override final def getMessage: String = + override def getMessage: String = s"Data source ${dataSource.identifier} did not complete request ${request.toString}." } diff --git a/core/src/main/scala/zquery/Result.scala b/core/src/main/scala/zquery/Result.scala index 1b36a04f6..083cf301a 100644 --- a/core/src/main/scala/zquery/Result.scala +++ b/core/src/main/scala/zquery/Result.scala @@ -1,6 +1,6 @@ package zquery -import zio.Cause +import zio.{ CanFail, Cause, NeedsEnv } import zquery.Result._ @@ -14,7 +14,7 @@ private[zquery] sealed trait Result[-R, +E, +A] { /** * Folds over the successful or failed result. */ - final def fold[B](failure: E => B, success: A => B): Result[R, Nothing, B] = this match { + final def fold[B](failure: E => B, success: A => B)(implicit ev: CanFail[E]): Result[R, Nothing, B] = this match { case Blocked(br, c) => blocked(br, c.fold(failure, success)) case Done(a) => done(success(a)) case Fail(e) => e.failureOrCause.fold(e => done(failure(e)), c => fail(c)) @@ -32,7 +32,7 @@ private[zquery] sealed trait Result[-R, +E, +A] { /** * Maps the specified function over the failed value of this result. */ - final def mapError[E1](f: E => E1): Result[R, E1, A] = this match { + final def mapError[E1](f: E => E1)(implicit ev: CanFail[E]): Result[R, E1, A] = this match { case Blocked(br, c) => blocked(br, c.mapError(f)) case Done(a) => done(a) case Fail(e) => fail(e.map(f)) @@ -41,14 +41,14 @@ private[zquery] sealed trait Result[-R, +E, +A] { /** * Provides this result with its required environment. */ - final def provide(name: String)(r: R): Result[Any, E, A] = - provideSome(s"_ => $name")(_ => r) + final def provide(r: Described[R])(implicit ev: NeedsEnv[R]): Result[Any, E, A] = + provideSome(Described(_ => r.value, s"_ => ${r.description}")) /** * Provides this result with part of its required environment. */ - final def provideSome[R0](name: String)(f: R0 => R): Result[R0, E, A] = this match { - case Blocked(br, c) => blocked(br.mapDataSources(DataSourceFunction.provideSome(name)(f)), c.provideSome(name)(f)) + final def provideSome[R0](f: Described[R0 => R])(implicit ev: NeedsEnv[R]): Result[R0, E, A] = this match { + case Blocked(br, c) => blocked(br.mapDataSources(DataSourceFunction.provideSome(f)), c.provideSome(f)) case Done(a) => done(a) case Fail(e) => fail(e) } @@ -60,25 +60,25 @@ private[zquery] object Result { * Constructs a result that is blocked on the specified requests with the * specified continuation. */ - final def blocked[R, E, A](blockedRequests: BlockedRequestMap[R], continue: ZQuery[R, E, A]): Result[R, E, A] = + def blocked[R, E, A](blockedRequests: BlockedRequestMap[R], continue: ZQuery[R, E, A]): Result[R, E, A] = Blocked(blockedRequests, continue) /** * Constructs a result that is done with the specified value. */ - final def done[A](value: A): Result[Any, Nothing, A] = + def done[A](value: A): Result[Any, Nothing, A] = Done(value) /** * Constructs a result that is failed with the specified `Cause`. */ - final def fail[E](cause: Cause[E]): Result[Any, E, Nothing] = + def fail[E](cause: Cause[E]): Result[Any, E, Nothing] = Fail(cause) /** * Lifts an `Either` into a result. */ - final def fromEither[E, A](either: Either[E, A]): Result[Any, E, A] = + def fromEither[E, A](either: Either[E, A]): Result[Any, E, A] = either.fold(e => Result.fail(Cause.fail(e)), a => Result.done(a)) final case class Blocked[-R, +E, +A](blockedRequests: BlockedRequestMap[R], continue: ZQuery[R, E, A]) diff --git a/core/src/main/scala/zquery/ZQuery.scala b/core/src/main/scala/zquery/ZQuery.scala index 0fced58af..6b8692e2c 100644 --- a/core/src/main/scala/zquery/ZQuery.scala +++ b/core/src/main/scala/zquery/ZQuery.scala @@ -1,6 +1,6 @@ package zquery -import zio.{ Cause, Ref, ZIO } +import zio.{ CanFail, Cause, NeedsEnv, Ref, ZIO } /** * A `ZQuery[R, E, A]` is a purely functional description of an effectual query @@ -77,12 +77,27 @@ sealed trait ZQuery[-R, +E, +A] { self => final def <*>[R1 <: R, E1 >: E, B](that: ZQuery[R1, E1, B]): ZQuery[R1, E1, (A, B)] = zip(that) + /** + * Returns a query whose failure and success channels have been mapped by the + * specified pair of functions, `f` and `g`. + */ + final def bimap[E1, B](f: E => E1, g: A => B)(implicit ev: CanFail[E]): ZQuery[R, E1, B] = + foldM(e => ZQuery.fail(f(e)), a => ZQuery.succeed(g(a))) + /** * A symbolic alias for `flatMap`. */ final def >>=[R1 <: R, E1 >: E, B](f: A => ZQuery[R1, E1, B]): ZQuery[R1, E1, B] = flatMap(f) + /** + * Returns a query whose failure and success have been lifted into an + * `Either`. The resulting query cannot fail, because the failure case has + * been exposed as part of the `Either` success case. + */ + final def either(implicit ev: CanFail[E]): ZQuery[R, Nothing, Either[E, A]] = + fold(Left(_), Right(_)) + /** * Returns a query that models execution of this query, followed by passing * its result to the specified function that returns a query. Requests @@ -105,10 +120,23 @@ sealed trait ZQuery[-R, +E, +A] { self => * that does not fail, but succeeds with the value returned by the left or * right function passed to `fold`. */ - final def fold[B](failure: E => B, success: A => B): ZQuery[R, Nothing, B] = - new ZQuery[R, Nothing, B] { - def step(cache: Cache): ZIO[R, Nothing, Result[R, Nothing, B]] = - self.step(cache).map(_.fold(failure, success)) + final def fold[B](failure: E => B, success: A => B)(implicit ev: CanFail[E]): ZQuery[R, Nothing, B] = + foldM(e => ZQuery.succeed(failure(e)), a => ZQuery.succeed(success(a))) + + /** + * Recovers from errors by accepting one query to execute for the case of an + * error, and one query to execute for the case of success. + */ + final def foldM[R1 <: R, E1, B](failure: E => ZQuery[R1, E1, B], success: A => ZQuery[R1, E1, B])( + implicit ev: CanFail[E] + ): ZQuery[R1, E1, B] = + new ZQuery[R1, E1, B] { + def step(cache: Cache): ZIO[R1, Nothing, Result[R1, E1, B]] = + self.step(cache).flatMap { + case Result.Blocked(br, c) => ZIO.succeed(Result.blocked(br, c.foldM(failure, success))) + case Result.Done(a) => success(a).step(cache) + case Result.Fail(e) => e.failureOrCause.fold(failure(_).step(cache), ZIO.halt) + } } /** @@ -123,25 +151,22 @@ sealed trait ZQuery[-R, +E, +A] { self => /** * Maps the specified function over the failed result of this query. */ - final def mapError[E1](f: E => E1): ZQuery[R, E1, A] = - new ZQuery[R, E1, A] { - def step(cache: Cache): ZIO[R, Nothing, Result[R, E1, A]] = - self.step(cache).map(_.mapError(f)) - } + final def mapError[E1](f: E => E1)(implicit ev: CanFail[E]): ZQuery[R, E1, A] = + bimap(f, identity) /** * Provides this query with its required environment. */ - final def provide(name: String)(r: R): ZQuery[Any, E, A] = - provideSome(s"_ => $name")(_ => r) + final def provide(r: Described[R])(implicit ev: NeedsEnv[R]): ZQuery[Any, E, A] = + provideSome(Described(_ => r.value, s"_ => ${r.description}")) /** * Provides this query with part of its required environment. */ - final def provideSome[R0](name: String)(f: R0 => R): ZQuery[R0, E, A] = + final def provideSome[R0](f: Described[R0 => R])(implicit ev: NeedsEnv[R]): ZQuery[R0, E, A] = new ZQuery[R0, E, A] { def step(cache: Cache): ZIO[R0, Nothing, Result[R0, E, A]] = - self.step(cache).provideSome(f).map(_.provideSome(name)(f)) + self.step(cache).provideSome(f.value).map(_.provideSome(f)) } /** @@ -251,20 +276,20 @@ object ZQuery { * their results. Requests will be executed sequentially and will not be * batched. */ - final def collectAll[R, E, A](as: Iterable[ZQuery[R, E, A]]): ZQuery[R, E, List[A]] = + def collectAll[R, E, A](as: Iterable[ZQuery[R, E, A]]): ZQuery[R, E, List[A]] = foreach(as)(identity) /** * Collects a collection of queries into a query returning a collection of * their results. All requests will be batched. */ - final def collectAllPar[R, E, A](as: Iterable[ZQuery[R, E, A]]): ZQuery[R, E, List[A]] = + def collectAllPar[R, E, A](as: Iterable[ZQuery[R, E, A]]): ZQuery[R, E, List[A]] = foreachPar(as)(identity) /** * Constructs a query that fails with the specified error. */ - final def fail[E](error: E): ZQuery[Any, E, Nothing] = + def fail[E](error: E): ZQuery[Any, E, Nothing] = ZQuery(ZIO.succeed(Result.fail(Cause.fail(error)))) /** @@ -272,7 +297,7 @@ object ZQuery { * into a query returning a collection of their results. Requests will be * executed sequentially and will not be batched. */ - final def foreach[R, E, A, B](as: Iterable[A])(f: A => ZQuery[R, E, B]): ZQuery[R, E, List[B]] = + def foreach[R, E, A, B](as: Iterable[A])(f: A => ZQuery[R, E, B]): ZQuery[R, E, List[B]] = as.foldRight[ZQuery[R, E, List[B]]](ZQuery.succeed(Nil))((a, bs) => f(a).zipWith(bs)(_ :: _)) /** @@ -280,80 +305,104 @@ object ZQuery { * into a query returning a collection of their results. All requests will be * batched. */ - final def foreachPar[R, E, A, B](as: Iterable[A])(f: A => ZQuery[R, E, B]): ZQuery[R, E, List[B]] = + def foreachPar[R, E, A, B](as: Iterable[A])(f: A => ZQuery[R, E, B]): ZQuery[R, E, List[B]] = as.foldRight[ZQuery[R, E, List[B]]](ZQuery.succeed(Nil))((a, bs) => f(a).zipWithPar(bs)(_ :: _)) /** * Constructs a query from an effect. */ - final def fromEffect[R, E, A](effect: ZIO[R, E, A]): ZQuery[R, E, A] = + def fromEffect[R, E, A](effect: ZIO[R, E, A]): ZQuery[R, E, A] = ZQuery(effect.foldCause(Result.fail, Result.done)) - /** - * Constructs a query from a request, requiring an environment containing a - * data source able to execute the request. This is useful to express the - * dependency on a data source in a more idiomatic style and to defer - * committing to a particular implementation of the data source too early, - * allowing, for example, for live and test implementations. - */ - final def fromRequest[R, E, A, B]( - request: A - )(implicit ev: A <:< Request[E, B]): ZQuery[R with DataSource[R, A], E, B] = - ZQuery.fromEffect(ZIO.environment[DataSource[R, A]]).flatMap(r => fromRequestWith(request)(r.dataSource)) - /** * Constructs a query from a request and a data source. Queries must be - * constructed with `fromRequestWith` or combinators derived from it for + * constructed with `fromRequest` or combinators derived from it for * optimizations to be applied. */ - final def fromRequestWith[R, E, A, B]( + def fromRequest[R, E, A, B]( request: A - )(dataSource: DataSource.Service[R, A])(implicit ev: A <:< Request[E, B]): ZQuery[R, E, B] = + )(dataSource: DataSource[R, A])(implicit ev: A <:< Request[E, B]): ZQuery[R, E, B] = new ZQuery[R, E, B] { def step(cache: Cache): ZIO[R, Nothing, Result[R, E, B]] = - cache.lookup(request).flatMap { - case None => - for { - ref <- Ref.make(Option.empty[Either[E, B]]) - _ <- cache.insert(request, ref) - } yield Result.blocked( - BlockedRequestMap(dataSource, BlockedRequest(request, ref)), - ZQuery { - ref.get.flatMap { - case None => ZIO.die(QueryFailure(dataSource, request)) - case Some(b) => ZIO.succeed(Result.fromEither(b)) + cache + .lookup(request) + .foldM( + _ => + for { + ref <- Ref.make(Option.empty[Either[E, B]]) + _ <- cache.insert(request, ref) + } yield Result.blocked( + BlockedRequestMap(dataSource, BlockedRequest(request, ref)), + ZQuery { + ref.get.flatMap { + case None => ZIO.die(QueryFailure(dataSource, request)) + case Some(b) => ZIO.succeed(Result.fromEither(b)) + } } - } - ) - case Some(ref) => - ref.get.map { - case Some(b) => Result.fromEither(b) - case None => - Result.blocked( - BlockedRequestMap.empty, - ZQuery { - ref.get.flatMap { - case None => ZIO.die(QueryFailure(dataSource, request)) - case Some(b) => ZIO.succeed(Result.fromEither(b)) + ), + ref => + ref.get.map { + case Some(b) => Result.fromEither(b) + case None => + Result.blocked( + BlockedRequestMap.empty, + ZQuery { + ref.get.flatMap { + case None => ZIO.die(QueryFailure(dataSource, request)) + case Some(b) => ZIO.succeed(Result.fromEither(b)) + } } - } - ) - } - } + ) + } + ) } + /** + * Performs a query for each element in a collection, collecting the results + * into a collection of failed results and a collection of successful + * results. Requests will be executed sequentially and will not be batched. + */ + def partitionM[R, E, A, B]( + as: Iterable[A] + )(f: A => ZQuery[R, E, B])(implicit ev: CanFail[E]): ZQuery[R, Nothing, (List[E], List[B])] = + ZQuery.foreach(as)(f(_).either).map(partitionMap(_)(identity)) + + /** + * Performs a query for each element in a collection, collecting the results + * into a collection of failed results and a collection of successful + * results. All requests will be batched. + */ + def partitionMPar[R, E, A, B]( + as: Iterable[A] + )(f: A => ZQuery[R, E, B])(implicit ev: CanFail[E]): ZQuery[R, Nothing, (List[E], List[B])] = + ZQuery.foreachPar(as)(f(_).either).map(partitionMap(_)(identity)) + /** * Constructs a query that succeeds with the specified value. */ - final def succeed[A](value: A): ZQuery[Any, Nothing, A] = + def succeed[A](value: A): ZQuery[Any, Nothing, A] = ZQuery(ZIO.succeed(Result.done(value))) /** * Constructs a query from an effect that returns a result. */ - private final def apply[R, E, A](step0: ZIO[R, Nothing, Result[R, E, A]]): ZQuery[R, E, A] = + private def apply[R, E, A](step0: ZIO[R, Nothing, Result[R, E, A]]): ZQuery[R, E, A] = new ZQuery[R, E, A] { def step(cache: Cache): ZIO[R, Nothing, Result[R, E, A]] = step0 } + + /** + * Partitions the elements of a collection using the specified function. + */ + private def partitionMap[E, A, B]( + as: Iterable[A] + )(f: A => Either[E, B])(implicit ev: CanFail[E]): (List[E], List[B]) = + as.foldRight((List.empty[E], List.empty[B])) { + case (a, (es, bs)) => + f(a).fold( + e => (e :: es, bs), + b => (es, b :: bs) + ) + } } diff --git a/core/src/test/scala/zquery/ZQuerySpec.scala b/core/src/test/scala/zquery/ZQuerySpec.scala index 3f3ef5f53..b0866a1f4 100644 --- a/core/src/test/scala/zquery/ZQuerySpec.scala +++ b/core/src/test/scala/zquery/ZQuerySpec.scala @@ -18,6 +18,7 @@ object ZQuerySpec } yield assert(log, hasSize(equalTo(2))) }, testM("mapError does not prevent batching") { + import zio.CanFail.canFail val a = getUserNameById(1).zip(getUserNameById(2)).mapError(identity) val b = getUserNameById(3).zip(getUserNameById(4)).mapError(identity) for { @@ -51,7 +52,7 @@ object ZQuerySpecUtil { final case class GetNameById(id: Int) extends UserRequest[String] val UserRequestDataSource = - DataSource.Service[Console, UserRequest[Any]]("UserRequestDataSource") { requests => + DataSource[Console, UserRequest[Any]]("UserRequestDataSource") { requests => console.putStrLn("Running query") *> ZIO.succeed { requests.foldLeft(CompletedRequestMap.empty) { case (completedRequests, GetAllIds) => completedRequests.insert(GetAllIds)(Right(userIds)) @@ -62,10 +63,10 @@ object ZQuerySpecUtil { } val getAllUserIds: ZQuery[Console, Nothing, List[Int]] = - ZQuery.fromRequestWith(GetAllIds)(UserRequestDataSource) + ZQuery.fromRequest(GetAllIds)(UserRequestDataSource) def getUserNameById(id: Int): ZQuery[Console, Nothing, String] = - ZQuery.fromRequestWith(GetNameById(id))(UserRequestDataSource) + ZQuery.fromRequest(GetNameById(id))(UserRequestDataSource) val getAllUserNames: ZQuery[Console, Nothing, List[String]] = for { diff --git a/examples/src/main/scala/caliban/optimizations/OptimizedTest.scala b/examples/src/main/scala/caliban/optimizations/OptimizedTest.scala index 2ce23f4cf..0db16146f 100644 --- a/examples/src/main/scala/caliban/optimizations/OptimizedTest.scala +++ b/examples/src/main/scala/caliban/optimizations/OptimizedTest.scala @@ -5,7 +5,7 @@ import caliban.schema.{ GenericSchema, Schema } import caliban.{ GraphQL, RootResolver } import zio.console.{ putStrLn, Console } import zio.{ App, ZIO } -import zquery.DataSource.Service.fromFunctionBatchedM +import zquery.DataSource.fromFunctionBatchedM import zquery.{ CompletedRequestMap, DataSource, Request, ZQuery } /** @@ -57,7 +57,7 @@ object OptimizedTest extends App with GenericSchema[Console] { ) case class GetUser(id: Int) extends Request[Nothing, User] - val UserDataSource: DataSource.Service[Console, GetUser] = DataSource.Service("UserDataSource") { requests => + val UserDataSource: DataSource[Console, GetUser] = DataSource("UserDataSource") { requests => requests.toList match { case head :: Nil => putStrLn("getUser").as(CompletedRequestMap.empty.insert(head)(Right(fakeUser(head.id)))) case list => @@ -68,51 +68,51 @@ object OptimizedTest extends App with GenericSchema[Console] { } case class GetEvent(id: Int) extends Request[Nothing, Event] - val EventDataSource: DataSource.Service[Console, GetEvent] = + val EventDataSource: DataSource[Console, GetEvent] = fromFunctionBatchedM("EventDataSource") { requests => putStrLn("getEvents").as(requests.map(r => fakeEvent(r.id))) } case class GetViewerMetadataForEvents(id: Int) extends Request[Nothing, ViewerMetadata] - val ViewerMetadataDataSource: DataSource.Service[Console, GetViewerMetadataForEvents] = + val ViewerMetadataDataSource: DataSource[Console, GetViewerMetadataForEvents] = fromFunctionBatchedM("ViewerMetadataDataSource") { requests => putStrLn("getViewerMetadataForEvents").as(requests.map(_ => ViewerMetadata(""))) } case class GetVenue(id: Int) extends Request[Nothing, Venue] - val VenueDataSource: DataSource.Service[Console, GetVenue] = + val VenueDataSource: DataSource[Console, GetVenue] = fromFunctionBatchedM("VenueDataSource") { requests => putStrLn("getVenues").as(requests.map(_ => Venue("venue"))) } case class GetTags(ids: List[Int]) extends Request[Nothing, List[Tag]] - val TagsDataSource: DataSource.Service[Console, GetTags] = + val TagsDataSource: DataSource[Console, GetTags] = fromFunctionBatchedM("TagsDataSource") { requests => putStrLn("getTags").as(requests.map(_.ids.map(id => Tag(id.toString)))) } case class GetViewerFriendIdsAttendingEvent(id: Int, first: Int) extends Request[Nothing, List[Int]] - val ViewerFriendDataSource: DataSource.Service[Console, GetViewerFriendIdsAttendingEvent] = + val ViewerFriendDataSource: DataSource[Console, GetViewerFriendIdsAttendingEvent] = fromFunctionBatchedM("ViewerFriendDataSource") { requests => putStrLn("getViewerFriendIdsAttendingEvent").as(requests.map(r => (1 to r.first).toList)) } case class GetUpcomingEventIdsForUser(id: Int, first: Int) extends Request[Nothing, List[Int]] - val UpcomingEventDataSource: DataSource.Service[Console, GetUpcomingEventIdsForUser] = + val UpcomingEventDataSource: DataSource[Console, GetUpcomingEventIdsForUser] = fromFunctionBatchedM("UpcomingEventDataSource") { requests => putStrLn("getUpcomingEventIdsForUser").as(requests.map(r => (1 to r.first).toList)) } - def getUser(id: Int): Query[User] = ZQuery.fromRequestWith(GetUser(id))(UserDataSource) - def getEvent(id: Int): Query[Event] = ZQuery.fromRequestWith(GetEvent(id))(EventDataSource) - def getVenue(id: Int): Query[Venue] = ZQuery.fromRequestWith(GetVenue(id))(VenueDataSource) - def getTags(ids: List[Int]): Query[List[Tag]] = ZQuery.fromRequestWith(GetTags(ids))(TagsDataSource) + def getUser(id: Int): Query[User] = ZQuery.fromRequest(GetUser(id))(UserDataSource) + def getEvent(id: Int): Query[Event] = ZQuery.fromRequest(GetEvent(id))(EventDataSource) + def getVenue(id: Int): Query[Venue] = ZQuery.fromRequest(GetVenue(id))(VenueDataSource) + def getTags(ids: List[Int]): Query[List[Tag]] = ZQuery.fromRequest(GetTags(ids))(TagsDataSource) def getViewerMetadataForEvent(id: Int): Query[ViewerMetadata] = - ZQuery.fromRequestWith(GetViewerMetadataForEvents(id))(ViewerMetadataDataSource) + ZQuery.fromRequest(GetViewerMetadataForEvents(id))(ViewerMetadataDataSource) def getViewerFriendIdsAttendingEvent(id: Int, first: Int): Query[List[Int]] = - ZQuery.fromRequestWith(GetViewerFriendIdsAttendingEvent(id, first))(ViewerFriendDataSource) + ZQuery.fromRequest(GetViewerFriendIdsAttendingEvent(id, first))(ViewerFriendDataSource) def getUpcomingEventIdsForUser(id: Int, first: Int): Query[List[Int]] = - ZQuery.fromRequestWith(GetUpcomingEventIdsForUser(id, first))(UpcomingEventDataSource) + ZQuery.fromRequest(GetUpcomingEventIdsForUser(id, first))(UpcomingEventDataSource) implicit val viewerMetadataSchema: Schema[Any, ViewerMetadata] = Schema.gen[ViewerMetadata] implicit val tagSchema: Schema[Any, Tag] = Schema.gen[Tag] diff --git a/vuepress/docs/docs/optimization.md b/vuepress/docs/docs/optimization.md index 3c922f78c..2d3637545 100644 --- a/vuepress/docs/docs/optimization.md +++ b/vuepress/docs/docs/optimization.md @@ -43,7 +43,7 @@ case class GetUserName(id: Int) extends Request[Throwable, String] Now let's build the corresponding `DataSource`. We need to implement the following functions: ```scala -val UserDataSource = new DataSource.Service[Any, GetUserName] { +val UserDataSource = new DataSource[Any, GetUserName] { override val identifier: String = ??? override def run(requests: Iterable[GetUserName]): ZIO[Any, Throwable, CompletedRequestMap] = ??? } @@ -77,18 +77,18 @@ override def run(requests: Iterable[GetUserName]): ZIO[Any, Nothing, CompletedRe } ``` -Now to build a `ZQuery` from it, we can use `ZQuery.fromRequestWith` and just pass the request and the data source: +Now to build a `ZQuery` from it, we can use `ZQuery.fromRequest` and just pass the request and the data source: ```scala def getUserNameById(id: Int): ZQuery[Any, Throwable, String] = - ZQuery.fromRequestWith(GetUserName(id))(UserDataSource) + ZQuery.fromRequest(GetUserName(id))(UserDataSource) ``` To run a `ZQuery`, simply use `ZQuery#run` which will return a `ZIO[R, E, A]`. ## ZQuery constructors and operators -There are several ways to create a `ZQuery`. We've seen `ZQuery.fromRequestWith`, but you can also: +There are several ways to create a `ZQuery`. We've seen `ZQuery.fromRequest`, but you can also: - create from a pure value with `ZQuery.succeed` - create from an effect value with `ZQuery.fromEffect`