├── .nvmrc ├── .github ├── CODEOWNERS ├── renovate.json ├── workflows │ ├── release-drafter.yml │ ├── auto-approve.yml │ └── ci.yml └── release-drafter.yml ├── .sbtopts ├── project ├── build.properties └── plugins.sbt ├── .vscode └── settings.json ├── .gitattributes ├── docs ├── package.json ├── sidebars.js ├── running_queries.md ├── creating-queries.md ├── creating-data-sources.md └── index.md ├── zio-query └── shared │ └── src │ ├── test │ └── scala │ │ └── zio │ │ └── query │ │ ├── ZIOBaseSpec.scala │ │ └── ZQuerySpec.scala │ └── main │ ├── scala-2.13 │ └── zio │ │ └── query │ │ └── UtilsVersionSpecific.scala │ ├── scala-3 │ └── zio │ │ └── query │ │ └── UtilsVersionSpecific.scala │ ├── scala-2.12 │ └── zio │ │ └── query │ │ └── UtilsVersionSpecific.scala │ └── scala │ └── zio │ └── query │ ├── package.scala │ ├── QueryFailure.scala │ ├── Request.scala │ ├── Described.scala │ ├── internal │ ├── QueryScope.scala │ ├── BlockedRequest.scala │ ├── Sequential.scala │ ├── Parallel.scala │ ├── Result.scala │ ├── Continue.scala │ └── BlockedRequests.scala │ ├── DataSourceAspect.scala │ ├── QueryAspect.scala │ ├── Cache.scala │ ├── CompletedRequestMap.scala │ └── DataSource.scala ├── .scalafix.conf ├── .scalafmt.conf ├── benchmarks └── src │ └── main │ └── scala │ └── zio │ └── query │ ├── BenchmarkUtil.scala │ ├── ZQueryBenchmark.scala │ ├── CollectAllBenchmark.scala │ ├── FromRequestBenchmark.scala │ └── DataSourceBenchmark.scala ├── README.md ├── .gitignore └── LICENSE /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.8.1 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @kyri-petrou 2 | -------------------------------------------------------------------------------- /.sbtopts: -------------------------------------------------------------------------------- 1 | -J-Xms1G 2 | -J-Xmx4G 3 | -J-XX:+UseG1GC 4 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.10.11 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.watcherExclude": { 3 | "**/target": true 4 | } 5 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | sbt linguist-vendored 2 | website/* linguist-vendored 3 | docs/* linguist-vendored 4 | 5 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@zio.dev/zio-query", 3 | "description": "ZIO Query Documentation", 4 | "license": "Apache-2.0" 5 | } 6 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "automerge": true, 3 | "rebaseWhen": "conflicted", 4 | "labels": ["type: dependencies"], 5 | "packageRules": [ 6 | { 7 | "matchManagers": [ 8 | "sbt" 9 | ], 10 | "enabled": false 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: ['master'] 6 | 7 | jobs: 8 | update_release_draft: 9 | runs-on: ubuntu-22.04 10 | steps: 11 | - uses: release-drafter/release-drafter@v5 12 | env: 13 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 14 | -------------------------------------------------------------------------------- /.github/workflows/auto-approve.yml: -------------------------------------------------------------------------------- 1 | name: Auto approve 2 | 3 | on: 4 | pull_request_target 5 | 6 | jobs: 7 | auto-approve: 8 | runs-on: ubuntu-22.04 9 | steps: 10 | - uses: hmarr/auto-approve-action@v3.2.1 11 | if: github.actor == 'scala-steward' || github.actor == 'renovate[bot]' 12 | with: 13 | github-token: "${{ secrets.GITHUB_TOKEN }}" 14 | -------------------------------------------------------------------------------- /docs/sidebars.js: -------------------------------------------------------------------------------- 1 | const sidebars = { 2 | sidebar: [ 3 | { 4 | type: "category", 5 | label: "ZIO Query", 6 | collapsed: false, 7 | link: { type: "doc", id: "index" }, 8 | items: [ 9 | "index", 10 | "creating-data-sources", 11 | "creating-queries", 12 | "running-queries" 13 | ] 14 | } 15 | ] 16 | }; 17 | 18 | module.exports = sidebars; 19 | -------------------------------------------------------------------------------- /zio-query/shared/src/test/scala/zio/query/ZIOBaseSpec.scala: -------------------------------------------------------------------------------- 1 | package zio.query 2 | 3 | import zio._ 4 | import zio.test._ 5 | 6 | trait ZIOBaseSpec extends ZIOSpecDefault { 7 | override def aspects: Chunk[TestAspectPoly] = 8 | if (TestPlatform.isJVM || TestPlatform.isNative) Chunk(TestAspect.timeout(60.seconds), TestAspect.timed) 9 | else Chunk(TestAspect.timeout(60.seconds), TestAspect.sequential, TestAspect.timed) 10 | } 11 | -------------------------------------------------------------------------------- /zio-query/shared/src/main/scala-2.13/zio/query/UtilsVersionSpecific.scala: -------------------------------------------------------------------------------- 1 | package zio.query 2 | 3 | import scala.collection.mutable 4 | 5 | private[query] object UtilsVersionSpecific { 6 | private final val DefaultLoadFactor = 0.75d 7 | 8 | def newHashMap[K, V](expectedNumElements: Int): mutable.HashMap[K, V] = 9 | new mutable.HashMap[K, V](sizeFor(expectedNumElements, DefaultLoadFactor), DefaultLoadFactor) 10 | 11 | private def sizeFor(nElements: Int, loadFactor: Double): Int = 12 | ((nElements + 1).toDouble / loadFactor).toInt 13 | } 14 | -------------------------------------------------------------------------------- /zio-query/shared/src/main/scala-3/zio/query/UtilsVersionSpecific.scala: -------------------------------------------------------------------------------- 1 | package zio.query 2 | 3 | import scala.collection.mutable 4 | 5 | private[query] object UtilsVersionSpecific { 6 | private final val DefaultLoadFactor = 0.75d 7 | 8 | def newHashMap[K, V](expectedNumElements: Int): mutable.HashMap[K, V] = 9 | new mutable.HashMap[K, V](sizeFor(expectedNumElements, DefaultLoadFactor), DefaultLoadFactor) 10 | 11 | private def sizeFor(nElements: Int, loadFactor: Double): Int = 12 | ((nElements + 1).toDouble / loadFactor).toInt 13 | } 14 | -------------------------------------------------------------------------------- /docs/running_queries.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: running-queries 3 | title: "Running Queries" 4 | --- 5 | 6 | There are several ways to run a `ZQuery`: 7 | 8 | - `runCache` runs the query using a given pre-populated cache. This can be useful for deterministically "replaying" a query without executing any new requests. 9 | - `runLog` runs the query and returns its result along with the cache containing a complete log of all requests executed and their results. This can be useful for logging or analysis of query execution. 10 | - `run` runs the query and returns its result. -------------------------------------------------------------------------------- /.scalafix.conf: -------------------------------------------------------------------------------- 1 | rules = [ 2 | Disable 3 | DisableSyntax 4 | ExplicitResultTypes 5 | LeakingImplicitClassVal 6 | NoAutoTupling 7 | NoValInForComprehension 8 | OrganizeImports 9 | ProcedureSyntax 10 | RemoveUnused 11 | ] 12 | 13 | Disable { 14 | ifSynthetic = [ 15 | "scala/Option.option2Iterable" 16 | "scala/Predef.any2stringadd" 17 | ] 18 | } 19 | 20 | OrganizeImports { 21 | # Allign with IntelliJ IDEA so that they don't fight each other 22 | groupedImports = Merge 23 | } 24 | 25 | RemoveUnused { 26 | imports = false // handled by OrganizeImports 27 | } 28 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | val zioSbtVersion = "0.4.0-alpha.31" 2 | 3 | addSbtPlugin("dev.zio" % "zio-sbt-ecosystem" % zioSbtVersion) 4 | addSbtPlugin("dev.zio" % "zio-sbt-website" % zioSbtVersion) 5 | 6 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.1") 7 | addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") 8 | addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1") 9 | addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.7") 10 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.18.2") 11 | 12 | resolvers ++= Resolver.sonatypeOssRepos("public") 13 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.7.14" 2 | maxColumn = 120 3 | align.preset = most 4 | align.multiline = false 5 | continuationIndent.defnSite = 2 6 | assumeStandardLibraryStripMargin = true 7 | docstrings.style = Asterisk 8 | docstrings.wrapMaxColumn = 80 9 | lineEndings = preserve 10 | includeCurlyBraceInSelectChains = false 11 | danglingParentheses.preset = true 12 | optIn.annotationNewlines = true 13 | newlines.alwaysBeforeMultilineDef = false 14 | runner.dialect = scala213 15 | rewrite.rules = [RedundantBraces] 16 | 17 | rewrite.redundantBraces.generalExpressions = false 18 | rewriteTokens = { 19 | "⇒": "=>" 20 | "→": "->" 21 | "←": "<-" 22 | } 23 | -------------------------------------------------------------------------------- /docs/creating-queries.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: creating-queries 3 | title: "Creating Queries" 4 | --- 5 | 6 | There are several ways to create a `ZQuery`. We've seen `ZQuery.fromRequest`, but you can also: 7 | 8 | - create a query from a pure value with `ZQuery.succeed` 9 | - create a query from an effect with `ZQuery.fromZIO` 10 | - combine multiple queries with `ZQuery.collectAllPar` and `ZQuery.foreachPar` and their sequential equivalents `ZQuery.collectAll` and `ZQuery.foreach` 11 | 12 | If you have a `ZQuery`, you can use: 13 | 14 | - `map` and `mapError` to modify the result or error type 15 | - `flatMap`, `zipWith`, or `zipWithPar` to combine it with other queries 16 | - `provide` and `provideSome` to eliminate some or all of its environmental requirements -------------------------------------------------------------------------------- /benchmarks/src/main/scala/zio/query/BenchmarkUtil.scala: -------------------------------------------------------------------------------- 1 | package zio.query 2 | 3 | import zio._ 4 | 5 | object BenchmarkUtil extends Runtime[Any] { self => 6 | val environment = Runtime.default.environment 7 | 8 | val fiberRefs = Runtime.default.fiberRefs 9 | 10 | val runtimeFlags = Runtime.default.runtimeFlags 11 | 12 | def unsafeRun[E, A](query: ZQuery[Any, E, A]): A = 13 | Unsafe.unsafe(implicit unsafe => self.unsafe.run(query.run).getOrThrowFiberFailure()) 14 | 15 | def unsafeRunCache[E, A](query: ZQuery[Any, E, A], cache: Cache): A = 16 | Unsafe.unsafe(implicit unsafe => self.unsafe.run(query.runCache(cache)).getOrThrowFiberFailure()) 17 | 18 | def unsafeRunZIO[E, A](query: ZIO[Any, E, A]): A = 19 | Unsafe.unsafe(implicit unsafe => self.unsafe.run(query).getOrThrowFiberFailure()) 20 | } 21 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | template: | 4 | # What's Changed 5 | $CHANGES 6 | categories: 7 | - title: 'Breaking' 8 | label: 'type: breaking' 9 | - title: 'New' 10 | label: 'type: feature' 11 | - title: 'Bug Fixes' 12 | label: 'type: bug' 13 | - title: 'Maintenance' 14 | label: 'type: maintenance' 15 | - title: 'Documentation' 16 | label: 'type: docs' 17 | - title: 'Dependency Updates' 18 | label: 'type: dependencies' 19 | 20 | version-resolver: 21 | major: 22 | labels: 23 | - 'type: breaking' 24 | minor: 25 | labels: 26 | - 'type: feature' 27 | patch: 28 | labels: 29 | - 'type: bug' 30 | - 'type: maintenance' 31 | - 'type: docs' 32 | - 'type: dependencies' 33 | - 'type: security' 34 | 35 | exclude-labels: 36 | - 'skip-changelog' 37 | -------------------------------------------------------------------------------- /zio-query/shared/src/main/scala-2.12/zio/query/UtilsVersionSpecific.scala: -------------------------------------------------------------------------------- 1 | package zio.query 2 | 3 | import zio.Chunk 4 | 5 | import scala.collection.mutable 6 | import scala.collection.mutable.ListBuffer 7 | 8 | private[query] object UtilsVersionSpecific { 9 | 10 | def newHashMap[K, V](expectedNumElements: Int): mutable.HashMap[K, V] = { 11 | val map = mutable.HashMap.empty[K, V] 12 | map.sizeHint(expectedNumElements) 13 | map 14 | } 15 | 16 | // Methods that don't exist in Scala 2.12 so we add them as syntax 17 | 18 | implicit class LiftCoSyntax[E, A, B](private val ev: A <:< Request[E, B]) extends AnyVal { 19 | def liftCo(in: Chunk[A]): Chunk[Request[E, B]] = in.asInstanceOf[Chunk[Request[E, B]]] 20 | } 21 | 22 | implicit class HashMapSyntax[K, V](private val map: collection.mutable.HashMap[K, V]) extends AnyVal { 23 | def addAll(elems: Iterable[(K, V)]): collection.mutable.HashMap[K, V] = map ++= elems 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /zio-query/shared/src/main/scala/zio/query/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2023 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package zio 18 | 19 | import zio.stacktracer.TracingImplicits.disableAutoTrace 20 | 21 | package object query { 22 | 23 | type RQuery[-R, +A] = ZQuery[R, Throwable, A] 24 | type URQuery[-R, +A] = ZQuery[R, Nothing, A] 25 | type Query[+E, +A] = ZQuery[Any, E, A] 26 | type UQuery[+A] = ZQuery[Any, Nothing, A] 27 | type TaskQuery[+A] = ZQuery[Any, Throwable, A] 28 | } 29 | -------------------------------------------------------------------------------- /zio-query/shared/src/main/scala/zio/query/QueryFailure.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2023 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package zio.query 18 | 19 | import zio.stacktracer.TracingImplicits.disableAutoTrace 20 | 21 | /** 22 | * `QueryFailure` keeps track of details relevant to query failures. 23 | */ 24 | final case class QueryFailure(dataSource: DataSource[Nothing, Nothing], request: Request[_, _]) 25 | extends Throwable(null, null, true, false) { 26 | override def getMessage: String = 27 | s"Data source ${dataSource.identifier} did not complete request ${request.toString}." 28 | } 29 | -------------------------------------------------------------------------------- /zio-query/shared/src/main/scala/zio/query/Request.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2023 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package zio.query 18 | 19 | import zio.stacktracer.TracingImplicits.disableAutoTrace 20 | 21 | /** 22 | * A `Request[E, A]` is a request from a data source for a value of type `A` 23 | * that may fail with an `E`. 24 | * 25 | * {{{ 26 | * sealed trait UserRequest[A] extends Request[Nothing, A] 27 | * 28 | * case object GetAllIds extends UserRequest[List[Int]] 29 | * final case class GetNameById(id: Int) extends UserRequest[String] 30 | * 31 | * }}} 32 | */ 33 | trait Request[E, A] 34 | -------------------------------------------------------------------------------- /benchmarks/src/main/scala/zio/query/ZQueryBenchmark.scala: -------------------------------------------------------------------------------- 1 | package zio.query 2 | 3 | import org.openjdk.jmh.annotations.{Scope => JScope, _} 4 | import zio.{Chunk, ZIO} 5 | import zio.query.BenchmarkUtil._ 6 | 7 | import java.util.concurrent.TimeUnit 8 | 9 | @Measurement(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) 10 | @Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) 11 | @Fork(1) 12 | @Threads(1) 13 | @State(JScope.Thread) 14 | @BenchmarkMode(Array(Mode.Throughput)) 15 | @OutputTimeUnit(TimeUnit.MILLISECONDS) 16 | class ZQueryBenchmark { 17 | val cache = Cache.unsafeMake() 18 | 19 | val qs1 = Chunk.fill(1000)(ZQuery.succeedNow("foo").runCache(cache)) 20 | val qs2 = Chunk.fill(1000)(ZQuery.succeed("foo").runCache(cache)) 21 | 22 | @Benchmark 23 | @OperationsPerInvocation(1000) 24 | def zQueryRunSucceedNowBenchmark() = 25 | unsafeRunZIO(ZIO.collectAllDiscard(qs1)) 26 | 27 | @Benchmark 28 | def zQuerySingleRunSucceedNowBenchmark() = 29 | unsafeRunZIO(qs1.head) 30 | 31 | @Benchmark 32 | @OperationsPerInvocation(1000) 33 | def zQueryRunSucceedBenchmark() = 34 | unsafeRunZIO(ZIO.collectAllDiscard(qs2)) 35 | 36 | @Benchmark 37 | def zQuerySingleRunSucceedBenchmark() = 38 | unsafeRunZIO(qs2.head) 39 | } 40 | -------------------------------------------------------------------------------- /zio-query/shared/src/main/scala/zio/query/Described.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2023 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package zio.query 18 | 19 | import zio.stacktracer.TracingImplicits.disableAutoTrace 20 | 21 | /** 22 | * A `Described[A]` is a value of type `A` along with a string description of 23 | * that value. The description may be used to generate a hash associated with 24 | * the value, so values that are equal should have the same description and 25 | * values that are not equal should have different descriptions. 26 | */ 27 | final case class Described[+A](value: A, description: String) 28 | 29 | object Described { 30 | 31 | implicit class AnySyntax[A](private val value: A) extends AnyVal { 32 | def ?(description: String): Described[A] = 33 | Described(value, description) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /zio-query/shared/src/main/scala/zio/query/internal/QueryScope.scala: -------------------------------------------------------------------------------- 1 | package zio.query.internal 2 | 3 | import zio._ 4 | import zio.stacktracer.TracingImplicits.disableAutoTrace 5 | 6 | import java.util.concurrent.atomic.AtomicReference 7 | 8 | /** 9 | * Lightweight variant of [[zio.Scope]], optimized for usage with ZQuery 10 | */ 11 | sealed trait QueryScope { 12 | def addFinalizerExit(f: Exit[Any, Any] => UIO[Any])(implicit trace: Trace): UIO[Unit] 13 | def closeAndExitWith[E, A](exit: Exit[E, A])(implicit trace: Trace): IO[E, A] 14 | } 15 | 16 | private[query] object QueryScope { 17 | def make(): QueryScope = new Default 18 | 19 | case object NoOp extends QueryScope { 20 | def addFinalizerExit(f: Exit[Any, Any] => UIO[Any])(implicit trace: Trace): UIO[Unit] = ZIO.unit 21 | def closeAndExitWith[E, A](exit: Exit[E, A])(implicit trace: Trace): IO[E, A] = exit 22 | } 23 | 24 | final private class Default extends QueryScope { 25 | private val ref = new AtomicReference(List.empty[Exit[Any, Any] => UIO[Any]]) 26 | 27 | def addFinalizerExit(f: Exit[Any, Any] => UIO[Any])(implicit trace: Trace): UIO[Unit] = 28 | ZIO.succeed { 29 | ref.updateAndGet(f :: _) 30 | () 31 | } 32 | 33 | def closeAndExitWith[E, A](exit: Exit[E, A])(implicit trace: Trace): IO[E, A] = { 34 | val finalizers = ref.get 35 | if (finalizers.isEmpty) exit 36 | else { 37 | ref.set(Nil) 38 | ZIO.foreachDiscard(finalizers)(_(exit)) *> exit 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /benchmarks/src/main/scala/zio/query/CollectAllBenchmark.scala: -------------------------------------------------------------------------------- 1 | package zio.query 2 | 3 | import org.openjdk.jmh.annotations.{Scope => JScope, _} 4 | import zio.query.BenchmarkUtil._ 5 | 6 | import java.util.concurrent.TimeUnit 7 | 8 | @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) 9 | @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) 10 | @Fork(2) 11 | @Threads(1) 12 | @State(JScope.Thread) 13 | @BenchmarkMode(Array(Mode.Throughput)) 14 | @OutputTimeUnit(TimeUnit.SECONDS) 15 | class CollectAllBenchmark { 16 | 17 | @Param(Array("100", "1000")) 18 | var count: Int = 100 19 | 20 | val parallelism: Int = 10 21 | 22 | @Benchmark 23 | def zQueryCollectAll(): Long = { 24 | val queries = (0 until count).map(_ => ZQuery.succeed(1)).toList 25 | val query = ZQuery.collectAll(queries).map(_.sum.toLong) 26 | unsafeRun(query) 27 | } 28 | 29 | @Benchmark 30 | def zQueryCollectAllBatched(): Long = { 31 | val queries = (0 until count).map(_ => ZQuery.succeed(1)).toList 32 | val query = ZQuery.collectAllBatched(queries).map(_.sum.toLong) 33 | unsafeRun(query) 34 | } 35 | 36 | @Benchmark 37 | def zQueryCollectAllPar(): Long = { 38 | val queries = (0 until count).map(_ => ZQuery.succeed(1)).toList 39 | val query = ZQuery.collectAllPar(queries).map(_.sum.toLong) 40 | unsafeRun(query) 41 | } 42 | 43 | @Benchmark 44 | def zQueryCollectAllParN(): Long = { 45 | val queries = (0 until count).map(_ => ZQuery.succeed(1)).toList 46 | val query = ZQuery.collectAllPar(queries).map(_.sum.toLong).withParallelism(parallelism) 47 | unsafeRun(query) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /zio-query/shared/src/main/scala/zio/query/internal/BlockedRequest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2023 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package zio.query.internal 18 | 19 | import zio.query.Request 20 | import zio.stacktracer.TracingImplicits.disableAutoTrace 21 | import zio.{Exit, Promise} 22 | 23 | /** 24 | * A `BlockedRequest[A]` keeps track of a request of type `A` along with a 25 | * `Promise` containing the result of the request, existentially hiding the 26 | * result type. This is used internally by the library to support data sources 27 | * that return different result types for different requests while guaranteeing 28 | * that results will be of the type requested. 29 | */ 30 | private[query] sealed trait BlockedRequest[+A] { 31 | type Failure 32 | type Success 33 | 34 | def request: Request[Failure, Success] 35 | 36 | def result: Promise[Failure, Success] 37 | 38 | override final def toString: String = 39 | s"BlockedRequest($request, $result)" 40 | } 41 | 42 | private[query] object BlockedRequest { 43 | 44 | def apply[E, A, B](request0: A, result0: Promise[E, B])(implicit 45 | ev: A <:< Request[E, B] 46 | ): BlockedRequest[A] = 47 | new BlockedRequest[A] { 48 | type Failure = E 49 | type Success = B 50 | 51 | val request = ev(request0) 52 | 53 | val result = result0 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /zio-query/shared/src/main/scala/zio/query/internal/Sequential.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2023 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package zio.query.internal 18 | 19 | import zio.Chunk 20 | import zio.query.DataSource 21 | import zio.stacktracer.TracingImplicits.disableAutoTrace 22 | 23 | /** 24 | * A `Sequential[R]` maintains a mapping from data sources to batches of 25 | * requests from those data sources that must be executed sequentially. 26 | */ 27 | private[query] final class Sequential[-R]( 28 | private val map: Map[DataSource[Any, Any], Chunk[Chunk[BlockedRequest[Any]]]] 29 | ) { self => 30 | 31 | /** 32 | * Combines this collection of batches of requests that must be executed 33 | * sequentially with that collection of batches of requests that must be 34 | * executed sequentially to return a new collection of batches of requests 35 | * that must be executed sequentially. 36 | */ 37 | def ++[R1 <: R](that: Sequential[R1]): Sequential[R1] = 38 | new Sequential( 39 | that.map.foldLeft(self.map) { case (map, (k, v)) => 40 | map + (k -> map.get(k).fold[Chunk[Chunk[BlockedRequest[Any]]]](v)(_ ++ v)) 41 | } 42 | ) 43 | 44 | /** 45 | * Returns whether this collection of batches of requests is empty. 46 | */ 47 | def isEmpty: Boolean = 48 | map.isEmpty 49 | 50 | def head: DataSource[R, Any] = 51 | map.head._1 52 | 53 | def size: Int = 54 | map.size 55 | 56 | /** 57 | * Converts this collection of batches requests that must be executed 58 | * sequentially to an `Iterable` containing mappings from data sources to 59 | * batches of requests from those data sources. 60 | */ 61 | def toIterable: Iterable[(DataSource[R, Any], Chunk[Chunk[BlockedRequest[Any]]])] = 62 | map 63 | } 64 | -------------------------------------------------------------------------------- /benchmarks/src/main/scala/zio/query/FromRequestBenchmark.scala: -------------------------------------------------------------------------------- 1 | package zio.query 2 | 3 | import org.openjdk.jmh.annotations.{Scope => JScope, _} 4 | import zio.query.BenchmarkUtil._ 5 | import zio.{Chunk, ZIO} 6 | 7 | import java.util.concurrent.TimeUnit 8 | 9 | @Measurement(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) 10 | @Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) 11 | @Fork(1) 12 | @Threads(1) 13 | @State(JScope.Thread) 14 | @BenchmarkMode(Array(Mode.Throughput)) 15 | @OutputTimeUnit(TimeUnit.SECONDS) 16 | class FromRequestBenchmark { 17 | 18 | @Param(Array("100", "1000")) 19 | var count: Int = 100 20 | 21 | var queries: Chunk[UQuery[Int]] = _ 22 | 23 | @Setup(Level.Trial) 24 | def setup() = 25 | queries = Chunk.fromIterable((0 until count).map(i => ds.query(Req(i)))) 26 | 27 | @Benchmark 28 | def fromRequestDefault(): Long = { 29 | val query = ZQuery.collectAllBatched(queries).map(_.sum.toLong) 30 | unsafeRun(query) 31 | } 32 | 33 | @Benchmark 34 | def fromRequestSized(): Long = { 35 | val query = ZQuery.collectAllBatched(queries).map(_.sum.toLong) 36 | unsafeRunCache(query, Cache.unsafeMake(count)) 37 | } 38 | 39 | @Benchmark 40 | def fromRequests(): Long = { 41 | val reqs = Chunk.fromIterable((0 until count).map(i => Req(i))) 42 | val query = ds.queryAll(reqs).map(_.sum.toLong) 43 | unsafeRunCache(query, Cache.unsafeMake(count)) 44 | } 45 | 46 | @Benchmark 47 | def fromRequestsCached(): Long = { 48 | val reqs = Chunk.fromIterable((0 until count).map(_ => Req(1))) 49 | val query = ds.queryAll(reqs).map(_.sum.toLong) 50 | unsafeRunCache(query, Cache.unsafeMake(count)) 51 | } 52 | 53 | @Benchmark 54 | def fromRequestUncached(): Long = { 55 | val query = ZQuery.collectAllBatched(queries).map(_.sum.toLong) 56 | unsafeRun(query.uncached) 57 | } 58 | 59 | @Benchmark 60 | def fromRequestZipRight(): Long = { 61 | val query = ZQuery.collectAllBatched(queries).map(_.sum.toLong) 62 | unsafeRun(query *> query *> query) 63 | } 64 | 65 | @Benchmark 66 | def fromRequestZipRightMemoized(): Long = { 67 | val f = ZQuery.collectAllBatched(queries).map(_.sum.toLong).memoize 68 | unsafeRun(ZQuery.unwrap(f.map(q => q *> q *> q))) 69 | } 70 | 71 | private case class Req(i: Int) extends Request[Nothing, Int] 72 | private val ds = DataSource.fromFunctionBatchedZIO("Datasource") { (reqs: Chunk[Req]) => ZIO.succeed(reqs.map(_.i)) } 73 | } 74 | -------------------------------------------------------------------------------- /zio-query/shared/src/main/scala/zio/query/internal/Parallel.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2023 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package zio.query.internal 18 | 19 | import zio.query.DataSource 20 | import zio.stacktracer.TracingImplicits.disableAutoTrace 21 | import zio.{Chunk, ChunkBuilder} 22 | 23 | import scala.collection.mutable 24 | 25 | /** 26 | * A `Parallel[R]` maintains a mapping from data sources to requests from those 27 | * data sources that can be executed in parallel. 28 | */ 29 | private[query] final class Parallel[-R]( 30 | private val map: mutable.HashMap[DataSource[?, ?], ChunkBuilder[BlockedRequest[Any]]] 31 | ) { self => 32 | 33 | def addOne[R1 <: R](dataSource: DataSource[R1, ?], blockedRequest: BlockedRequest[Any]): Parallel[R1] = { 34 | self.map.getOrElseUpdate(dataSource, Chunk.newBuilder) addOne blockedRequest 35 | self 36 | } 37 | 38 | /** 39 | * Returns whether this collection of requests is empty. 40 | */ 41 | def isEmpty: Boolean = 42 | map.isEmpty 43 | 44 | def head: DataSource[R, Any] = 45 | map.head._1.asInstanceOf[DataSource[R, Any]] 46 | 47 | def size: Int = 48 | map.size 49 | 50 | /** 51 | * Converts this collection of requests that can be executed in parallel to a 52 | * batch of requests in a collection of requests that must be executed 53 | * sequentially. 54 | */ 55 | def sequential: Sequential[R] = { 56 | val builder = Map.newBuilder[DataSource[Any, Any], Chunk[Chunk[BlockedRequest[Any]]]] 57 | map.foreach { case (dataSource, chunkBuilder) => 58 | builder += ((dataSource.asInstanceOf[DataSource[Any, Any]], Chunk.single(chunkBuilder.result()))) 59 | } 60 | new Sequential(builder.result()) 61 | } 62 | } 63 | 64 | private[query] object Parallel { 65 | 66 | /** 67 | * The empty collection of requests. 68 | */ 69 | def empty[R]: Parallel[R] = 70 | new Parallel(mutable.HashMap.empty) 71 | } 72 | -------------------------------------------------------------------------------- /zio-query/shared/src/main/scala/zio/query/DataSourceAspect.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2023 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package zio.query 18 | 19 | import zio.stacktracer.TracingImplicits.disableAutoTrace 20 | import zio.{Chunk, Trace, ZIO} 21 | 22 | /** 23 | * A `DataSourceAspect` is an aspect that can be weaved into queries. You can 24 | * think of an aspect as a polymorphic function, capable of transforming one 25 | * data source into another, possibly enlarging the environment type. 26 | */ 27 | trait DataSourceAspect[-R] { self => 28 | 29 | /** 30 | * Applies the aspect to a data source. 31 | */ 32 | def apply[R1 <: R, A](dataSource: DataSource[R1, A]): DataSource[R1, A] 33 | 34 | /** 35 | * A symbolic alias for `andThen`. 36 | */ 37 | final def >>>[R1 <: R](that: DataSourceAspect[R1]): DataSourceAspect[R1] = 38 | andThen(that) 39 | 40 | /** 41 | * Returns a new aspect that represents the sequential composition of this 42 | * aspect with the specified one. 43 | */ 44 | final def andThen[R1 <: R](that: DataSourceAspect[R1]): DataSourceAspect[R1] = 45 | new DataSourceAspect[R1] { 46 | def apply[R2 <: R1, A](dataSource: DataSource[R2, A]): DataSource[R2, A] = 47 | that(self(dataSource)) 48 | } 49 | } 50 | 51 | object DataSourceAspect { 52 | 53 | /** 54 | * A data source aspect that executes requests between two effects, `before` 55 | * and `after`, where the result of `before` can be used by `after`. 56 | */ 57 | def around[R, A]( 58 | before: Described[ZIO[R, Nothing, A]] 59 | )(after: Described[A => ZIO[R, Nothing, Any]]): DataSourceAspect[R] = 60 | new DataSourceAspect[R] { 61 | def apply[R1 <: R, A](dataSource: DataSource[R1, A]): DataSource[R1, A] = 62 | new DataSource[R1, A] { 63 | val identifier = s"${dataSource.identifier} @@ around(${before.description})(${after.description})" 64 | def runAll(requests: Chunk[Chunk[A]])(implicit trace: Trace): ZIO[R1, Nothing, CompletedRequestMap] = 65 | ZIO.acquireReleaseWith(before.value)(after.value)(_ => dataSource.runAll(requests)) 66 | } 67 | } 68 | 69 | /** 70 | * A data source aspect that limits data sources to executing at most `n` 71 | * requests in parallel. 72 | */ 73 | def maxBatchSize(n: Int): DataSourceAspect[Any] = 74 | new DataSourceAspect[Any] { 75 | def apply[R, A](dataSource: DataSource[R, A]): DataSource[R, A] = 76 | dataSource.batchN(n) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /docs/creating-data-sources.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: creating-data-sources 3 | title: "Creating Data Sources" 4 | --- 5 | 6 | To construct a `ZQuery` that executes a request, you first need to create a `DataSource`. A `DataSource[R, A]` requires an environment `R` and is capable of executing requests of type `A`. It is defined in terms of: 7 | 8 | - an `identifier` that uniquely identifies the data source 9 | - an effectual function `runAll` from a `Chunk[Chunk[A]]` of requests to a `CompletedRequestMap` of requests and results 10 | 11 | The outer `Chunk` represents batches of requests that must be performed sequentially. The inner `Chunk` represents a batch of requests that can be performed in parallel. This allows data sources to introspect on all the requests being executed and optimize the query. 12 | 13 | ```scala mdoc:invisible 14 | import zio._ 15 | import zio.query._ 16 | ``` 17 | 18 | Let's consider `getUserNameById` from the previous example. 19 | 20 | We need to define a corresponding request type that extends `Request` for a given response type: 21 | 22 | ```scala mdoc:silent 23 | case class GetUserName(id: Int) extends Request[Throwable, String] 24 | ``` 25 | 26 | Now let's build the corresponding `DataSource`. We will create a `Batched` data source that executes requests that can be performed in parallel in batches but does not further optimize batches of requests that must be performed sequentially. We need to implement the following functions: 27 | 28 | ```scala mdoc:silent 29 | lazy val UserDataSource = new DataSource.Batched[Any, GetUserName] { 30 | val identifier: String = ??? 31 | def run(requests: Chunk[GetUserName])(implicit trace: Trace): ZIO[Any, Nothing, CompletedRequestMap] = ??? 32 | } 33 | ``` 34 | 35 | We will use "UserDataSource" as our identifier. This name should not be reused for other data sources. 36 | 37 | ```scala mdoc:silent 38 | val identifier: String = "UserDataSource" 39 | ``` 40 | 41 | We will define two different behaviors depending on whether we receive a single request or multiple requests at once. For each request, we need to insert into the result map a value of type `Exit` (`fail` for an error and `succeed` for a success). 42 | 43 | ```scala mdoc:silent 44 | def run(requests: Chunk[GetUserName]): ZIO[Any, Nothing, CompletedRequestMap] = 45 | requests.toList match { 46 | case request :: Nil => 47 | // get user by ID e.g. SELECT name FROM users WHERE id = $id 48 | val result: Task[String] = ??? 49 | result.exit.map(CompletedRequestMap.single(request, _)) 50 | case batch => 51 | // get multiple users at once e.g. SELECT id, name FROM users WHERE id IN ($ids) 52 | val result: Task[List[(Int, String)]] = ??? 53 | result.foldCause( 54 | CompletedRequestMap.failCause(requests, _), 55 | CompletedRequestMap.fromIterableWith(_)(kv => GetUserName(kv._1), kv => Exit.succeed(kv._2)) 56 | ) 57 | } 58 | ``` 59 | 60 | Now to build a `ZQuery`, we can use `ZQuery.fromRequest` and just pass the request and the data source: 61 | 62 | ```scala mdoc:silent 63 | def getUserNameById(id: Int): ZQuery[Any, Throwable, String] = 64 | ZQuery.fromRequest(GetUserName(id))(UserDataSource) 65 | ``` 66 | 67 | To run a `ZQuery`, simply use `ZQuery#run` which will return a `ZIO[R, E, A]`. -------------------------------------------------------------------------------- /benchmarks/src/main/scala/zio/query/DataSourceBenchmark.scala: -------------------------------------------------------------------------------- 1 | package zio.query 2 | 3 | import cats.effect.IO 4 | import cats.effect.unsafe.implicits._ 5 | import cats.syntax.all._ 6 | import fetch.{Fetch, fetchM} 7 | import org.openjdk.jmh.annotations.{Scope => JScope, _} 8 | import zio.query.BenchmarkUtil._ 9 | import zio.{Chunk, ZIO} 10 | 11 | import java.util.concurrent.TimeUnit 12 | 13 | @Measurement(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) 14 | @Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) 15 | @Fork(1) 16 | @Threads(1) 17 | @State(JScope.Thread) 18 | @BenchmarkMode(Array(Mode.Throughput)) 19 | @OutputTimeUnit(TimeUnit.SECONDS) 20 | class DataSourceBenchmark { 21 | 22 | /* Results as of 13/04/2024 (v0.7.0): 23 | * [info] Benchmark (count) Mode Cnt Score Error Units 24 | * [info] DataSourceBenchmark.fetchSumDuplicatedBenchmark 100 thrpt 3 13079.078 ± 1766.290 ops/s 25 | * [info] DataSourceBenchmark.fetchSumDuplicatedBenchmark 1000 thrpt 3 1750.691 ± 515.567 ops/s 26 | * [info] DataSourceBenchmark.fetchSumUniqueBenchmark 100 thrpt 3 1452.417 ± 301.895 ops/s 27 | * [info] DataSourceBenchmark.fetchSumUniqueBenchmark 1000 thrpt 3 170.926 ± 16.258 ops/s 28 | * [info] DataSourceBenchmark.zquerySumDuplicatedBenchmark 100 thrpt 3 50938.486 ± 2185.032 ops/s 29 | * [info] DataSourceBenchmark.zquerySumDuplicatedBenchmark 1000 thrpt 3 6260.981 ± 137.187 ops/s 30 | * [info] DataSourceBenchmark.zquerySumUniqueBenchmark 100 thrpt 3 38385.921 ± 4523.814 ops/s 31 | * [info] DataSourceBenchmark.zquerySumUniqueBenchmark 1000 thrpt 3 4287.090 ± 688.067 ops/s 32 | */ 33 | 34 | @Param(Array("100", "1000")) 35 | var count: Int = 100 36 | 37 | @Benchmark 38 | def zquerySumDuplicatedBenchmark(): Long = { 39 | import ZQueryImpl._ 40 | 41 | val reqs = (0 until count).toList.map(i => ZQuery.fromRequest(Req(1))(ds)) 42 | val query = ZQuery.collectAllBatched(reqs).map(_.sum.toLong) 43 | unsafeRun(query) 44 | } 45 | 46 | @Benchmark 47 | def zquerySumUniqueBenchmark(): Long = { 48 | import ZQueryImpl._ 49 | 50 | val reqs = (0 until count).toList.map(i => ZQuery.fromRequest(Req(i))(ds)) 51 | val query = ZQuery.collectAllBatched(reqs).map(_.sum.toLong) 52 | unsafeRun(query) 53 | } 54 | 55 | @Benchmark 56 | def fetchSumDuplicatedBenchmark(): Long = { 57 | import FetchImpl._ 58 | import fetch.fetchM 59 | type FIO[A] = Fetch[IO, A] 60 | 61 | val reqs = (0 until count).toList.map(i => fetchPlusOne(1)) 62 | val query = reqs.sequence[FIO, Int].map(_.sum) 63 | Fetch.run(query).unsafeRunSync() 64 | } 65 | 66 | @Benchmark 67 | def fetchSumUniqueBenchmark(): Long = { 68 | import fetch.fetchM 69 | import FetchImpl._ 70 | type FIO[A] = Fetch[IO, A] 71 | 72 | val reqs = (0 until count).toList.map(i => fetchPlusOne(i)) 73 | val query = reqs.sequence[FIO, Int].map(_.sum) 74 | Fetch.run[IO](query).unsafeRunSync() 75 | } 76 | 77 | object ZQueryImpl { 78 | case class Req(i: Int) extends Request[Nothing, Int] 79 | val ds = DataSource.fromFunctionBatchedZIO("PlusOne") { (reqs: Chunk[Req]) => ZIO.succeed(reqs.map(_.i + 1)) } 80 | } 81 | 82 | object FetchImpl { 83 | import cats.data.NonEmptyList 84 | import cats.effect._ 85 | import fetch._ 86 | 87 | object PlusOne extends Data[Int, Int] { 88 | def name = "PlusOne" 89 | 90 | val source: DataSource[IO, Int, Int] = new DataSource[IO, Int, Int] { 91 | override def data: Data[Int, Int] = PlusOne 92 | 93 | override def CF: Concurrent[IO] = Concurrent[IO] 94 | 95 | override def fetch(id: Int): IO[Option[Int]] = 96 | IO(Some(id + 1)) 97 | 98 | override def batch(ids: NonEmptyList[Int]): IO[Map[Int, Int]] = 99 | IO(ids.toList.view.map(id => id -> (id + 1)).toMap) 100 | } 101 | } 102 | 103 | def fetchPlusOne(n: Int): Fetch[IO, Int] = 104 | Fetch(n, PlusOne.source) 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /zio-query/shared/src/main/scala/zio/query/QueryAspect.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package zio.query 18 | 19 | import zio._ 20 | 21 | /** 22 | * A `QueryAspect` is an aspect that can be weaved into queries. You can think 23 | * of an aspect as a polymorphic function, capable of transforming one data 24 | * source into another, possibly enlarging the environment type. 25 | */ 26 | trait QueryAspect[+LowerR, -UpperR, +LowerE, -UpperE, +LowerA, -UpperA] { self => 27 | 28 | /** 29 | * Applies the aspect to a query. 30 | */ 31 | def apply[R >: LowerR <: UpperR, E >: LowerE <: UpperE, A >: LowerA <: UpperA](query: ZQuery[R, E, A])(implicit 32 | trace: Trace 33 | ): ZQuery[R, E, A] 34 | 35 | /** 36 | * A symbolic alias for `andThen`. 37 | */ 38 | def >>>[ 39 | LowerR1 >: LowerR, 40 | UpperR1 <: UpperR, 41 | LowerE1 >: LowerE, 42 | UpperE1 <: UpperE, 43 | LowerA1 >: LowerA, 44 | UpperA1 <: UpperA 45 | ]( 46 | that: QueryAspect[LowerR1, UpperR1, LowerE1, UpperE1, LowerA1, UpperA1] 47 | ): QueryAspect[LowerR1, UpperR1, LowerE1, UpperE1, LowerA1, UpperA1] = 48 | self.andThen(that) 49 | 50 | /** 51 | * Returns a new aspect that represents the sequential composition of this 52 | * aspect with the specified one. 53 | */ 54 | def andThen[ 55 | LowerR1 >: LowerR, 56 | UpperR1 <: UpperR, 57 | LowerE1 >: LowerE, 58 | UpperE1 <: UpperE, 59 | LowerA1 >: LowerA, 60 | UpperA1 <: UpperA 61 | ]( 62 | that: QueryAspect[LowerR1, UpperR1, LowerE1, UpperE1, LowerA1, UpperA1] 63 | ): QueryAspect[LowerR1, UpperR1, LowerE1, UpperE1, LowerA1, UpperA1] = 64 | new QueryAspect[LowerR1, UpperR1, LowerE1, UpperE1, LowerA1, UpperA1] { 65 | def apply[R >: LowerR1 <: UpperR1, E >: LowerE1 <: UpperE1, A >: LowerA1 <: UpperA1]( 66 | query: ZQuery[R, E, A] 67 | )(implicit trace: Trace): ZQuery[R, E, A] = 68 | that(self(query)) 69 | } 70 | } 71 | 72 | object QueryAspect { 73 | 74 | /** 75 | * A query aspect that executes queries between two effects, `before` and 76 | * `after`, where the result of `before` can be used by `after`. 77 | */ 78 | def around[R, A]( 79 | before: ZIO[R, Nothing, A] 80 | )(after: A => ZIO[R, Nothing, Any]): QueryAspect[Nothing, R, Nothing, Any, Nothing, Any] = 81 | new QueryAspect[Nothing, R, Nothing, Any, Nothing, Any] { 82 | def apply[R1 <: R, E, B](query: ZQuery[R1, E, B])(implicit trace: Trace): ZQuery[R1, E, B] = 83 | ZQuery.acquireReleaseWith(before)(after)(_ => query) 84 | } 85 | 86 | /** 87 | * A query aspect that executes requests between two effects, `before` and 88 | * `after`, where the result of `before` can be used by `after`. 89 | */ 90 | def aroundDataSource[R, A]( 91 | before: Described[ZIO[R, Nothing, A]] 92 | )(after: Described[A => ZIO[R, Nothing, Any]]): QueryAspect[Nothing, R, Nothing, Any, Nothing, Any] = 93 | fromDataSourceAspect(DataSourceAspect.around(before)(after)) 94 | 95 | /** 96 | * A query aspect that executes requests with the specified data source 97 | * aspect. 98 | */ 99 | def fromDataSourceAspect[R]( 100 | dataSourceAspect: DataSourceAspect[R] 101 | ): QueryAspect[Nothing, R, Nothing, Any, Nothing, Any] = 102 | new QueryAspect[Nothing, R, Nothing, Any, Nothing, Any] { 103 | def apply[R1 <: R, E, A](query: ZQuery[R1, E, A])(implicit trace: Trace): ZQuery[R1, E, A] = 104 | query.mapDataSources(dataSourceAspect) 105 | } 106 | 107 | /** 108 | * A query aspect that limits data sources to executing at most `n` requests 109 | * in parallel. 110 | */ 111 | def maxBatchSize(n: Int): QueryAspect[Nothing, Any, Nothing, Any, Nothing, Any] = 112 | fromDataSourceAspect(DataSourceAspect.maxBatchSize(n)) 113 | } 114 | -------------------------------------------------------------------------------- /zio-query/shared/src/main/scala/zio/query/Cache.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2023 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package zio.query 18 | 19 | import zio._ 20 | import zio.stacktracer.TracingImplicits.disableAutoTrace 21 | 22 | import java.util.concurrent.ConcurrentHashMap 23 | 24 | /** 25 | * A `Cache` maintains an internal state with a mapping from requests to 26 | * `Promise`s that will contain the result of those requests when they are 27 | * executed. This is used internally by the library to provide deduplication and 28 | * caching of requests. 29 | */ 30 | trait Cache { 31 | 32 | /** 33 | * Looks up a request in the cache, failing with the unit value if the request 34 | * is not in the cache or succeeding with a `Promise` if the request is in the 35 | * cache that will contain the result of the request when it is executed. 36 | */ 37 | def get[E, A](request: Request[E, A])(implicit trace: Trace): IO[Unit, Promise[E, A]] 38 | 39 | /** 40 | * Looks up a request in the cache. If the request is not in the cache returns 41 | * a `Left` with a `Promise` that can be completed to complete the request. If 42 | * the request is in the cache returns a `Right` with a `Promise` that will 43 | * contain the result of the request when it is executed. 44 | */ 45 | def lookup[E, A, B](request: A)(implicit 46 | ev: A <:< Request[E, B], 47 | trace: Trace 48 | ): UIO[Either[Promise[E, B], Promise[E, B]]] 49 | 50 | /** 51 | * Inserts a request and a `Promise` that will contain the result of the 52 | * request when it is executed into the cache. 53 | */ 54 | def put[E, A](request: Request[E, A], result: Promise[E, A])(implicit trace: Trace): UIO[Unit] 55 | 56 | /** 57 | * Removes a request from the cache. 58 | */ 59 | def remove[E, A](request: Request[E, A])(implicit trace: Trace): UIO[Unit] 60 | } 61 | 62 | object Cache { 63 | 64 | /** 65 | * Constructs an empty cache. 66 | */ 67 | def empty(implicit trace: Trace): UIO[Cache] = 68 | ZIO.succeed(Cache.unsafeMake()) 69 | 70 | /** 71 | * Constructs an empty cache, sized to accommodate the specified number of 72 | * elements without the need for the internal data structures to be resized. 73 | */ 74 | def empty(expectedNumOfElements: Int)(implicit trace: Trace): UIO[Cache] = 75 | ZIO.succeed(Cache.unsafeMake(expectedNumOfElements)) 76 | 77 | private[query] final class Default(private val map: ConcurrentHashMap[Request[_, _], Promise[_, _]]) extends Cache { 78 | 79 | def get[E, A](request: Request[E, A])(implicit trace: Trace): IO[Unit, Promise[E, A]] = 80 | ZIO.suspendSucceed { 81 | val out = map.get(request).asInstanceOf[Promise[E, A]] 82 | if (out eq null) Exit.fail(()) else Exit.succeed(out) 83 | } 84 | 85 | def lookup[E, A, B](request: A)(implicit 86 | ev: A <:< Request[E, B], 87 | trace: Trace 88 | ): UIO[Either[Promise[E, B], Promise[E, B]]] = 89 | ZIO.succeed(lookupUnsafe(request)(Unsafe.unsafe)) 90 | 91 | def lookupUnsafe[E, A, B](request: Request[_, _])(implicit 92 | unsafe: Unsafe 93 | ): Either[Promise[E, B], Promise[E, B]] = { 94 | val newPromise = Promise.unsafe.make[E, B](FiberId.None) 95 | val existing = map.putIfAbsent(request, newPromise).asInstanceOf[Promise[E, B]] 96 | if (existing eq null) Left(newPromise) else Right(existing) 97 | } 98 | 99 | def put[E, A](request: Request[E, A], result: Promise[E, A])(implicit trace: Trace): UIO[Unit] = 100 | ZIO.succeed(map.put(request, result)) 101 | 102 | def remove[E, A](request: Request[E, A])(implicit trace: Trace): UIO[Unit] = 103 | ZIO.succeed(map.remove(request)) 104 | } 105 | 106 | // TODO: Initialize the map with a sensible default value. Default is 16, which seems way too small for a cache 107 | private[query] def unsafeMake(): Cache = new Default(new ConcurrentHashMap()) 108 | 109 | private[query] def unsafeMake(expectedNumOfElements: Int): Cache = { 110 | val initialSize = Math.ceil(expectedNumOfElements / 0.75d).toInt 111 | new Default(new ConcurrentHashMap(initialSize)) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: index 3 | title: "Introduction to ZIO Query" 4 | sidebar_label: "ZIO Query" 5 | --- 6 | 7 | [ZIO Query](https://github.com/zio/zio-query) is a library for writing optimized queries to data sources in a high-level compositional style. It can add efficient pipelining, batching, and caching to any data source. ZIO Query helps us dramatically reduce load on data sources and improve performance. 8 | 9 | @PROJECT_BADGES@ 10 | 11 | ## Introduction 12 | 13 | Some key features of ZIO Query: 14 | 15 | - **Batching** — ZIO Query detects parts of composite queries that can be executed in parallel without changing the semantics of the query. 16 | - **Pipelining** — ZIO Query detects parts of composite queries that can be combined together for fewer individual requests to the data source. 17 | - **Caching** — ZIO Query can transparently cache read queries to minimize the cost of fetching the same item repeatedly in the scope of a query. 18 | 19 | Compared with Fetch, ZIO Query supports response types that depend on request types, does not require higher-kinded types and implicits, supports ZIO environment and statically typed errors, and has no dependencies except for ZIO. 20 | 21 | A `ZQuery[R, E, A]` is a purely functional description of an effectual query that may contain requests from one or more data sources, requires an environment `R`, and may fail with an `E` or succeed with an `A`. 22 | 23 | Requests that can be performed in parallel, as expressed by `zipWithPar` and combinators derived from it, will automatically be batched. Requests that must be performed sequentially, as expressed by `zipWith` and combinators derived from it, will automatically be pipelined. This allows for aggressive data source specific optimizations. Requests can also be deduplicated and cached. 24 | 25 | ```scala mdoc:invisible 26 | import zio._ 27 | import zio.query._ 28 | ``` 29 | 30 | This allows for writing queries in a high level, compositional style, with confidence that they will automatically be optimized. For example, consider the following query from a user service. 31 | 32 | Assume we have the following database access layer APIs: 33 | 34 | ```scala mdoc:silent 35 | def getAllUserIds: ZIO[Any, Nothing, List[Int]] = { 36 | // Get all user IDs e.g. SELECT id FROM users 37 | ZIO.succeed(???) 38 | } 39 | 40 | def getUserNameById(id: Int): ZIO[Any, Nothing, String] = { 41 | // Get user by ID e.g. SELECT name FROM users WHERE id = $id 42 | ZIO.succeed(???) 43 | } 44 | ``` 45 | 46 | We can get their corresponding usernames from the database by the following code snippet: 47 | 48 | ```scala mdoc:silent 49 | val userNames = for { 50 | ids <- getAllUserIds 51 | names <- ZIO.foreachPar(ids)(getUserNameById) 52 | } yield names 53 | ``` 54 | 55 | It works, but this is not performant. It is going to query the underlying database _N + 1_ times, one for `getAllUserIds` and one for each call to `getUserNameById`. 56 | 57 | In contrast, `ZQuery` will automatically optimize this to two queries, one for `userIds` and one for `userNames`: 58 | 59 | ```scala mdoc:silent:nest 60 | lazy val getAllUserIds: ZQuery[Any, Nothing, List[Int]] = ??? 61 | def getUserNameById(id: Int): ZQuery[Any, Nothing, String] = ??? 62 | 63 | lazy val userQuery: ZQuery[Any, Nothing, List[String]] = for { 64 | userIds <- getAllUserIds 65 | userNames <- ZQuery.foreachPar(userIds)(getUserNameById) 66 | } yield userNames 67 | ``` 68 | 69 | ## Installation 70 | 71 | In order to use this library, we need to add the following line in our `build.sbt` file: 72 | 73 | ```scala 74 | libraryDependencies += "dev.zio" %% "zio-query" % "@VERSION@" 75 | ``` 76 | 77 | ## Example 78 | 79 | Here is an example of using ZIO Query, which optimizes multiple database queries by batching all of them in one query: 80 | 81 | ```scala mdoc:compile-only 82 | import zio._ 83 | import zio.query._ 84 | 85 | object ZQueryExample extends ZIOAppDefault { 86 | case class GetUserName(id: Int) extends Request[Throwable, String] 87 | 88 | lazy val UserDataSource: DataSource.Batched[Any, GetUserName] = 89 | new DataSource.Batched[Any, GetUserName] { 90 | val identifier: String = "UserDataSource" 91 | 92 | def run(requests: Chunk[GetUserName])(implicit trace: Trace): ZIO[Any, Nothing, CompletedRequestMap] = 93 | requests.toList match { 94 | case request :: Nil => 95 | val result: Task[String] = { 96 | // get user by ID e.g. SELECT name FROM users WHERE id = $id 97 | ZIO.succeed(???) 98 | } 99 | 100 | result.exit.map(CompletedRequestMap.single(request, _)) 101 | 102 | case batch: Seq[GetUserName] => 103 | val result: Task[List[(Int, String)]] = { 104 | // get multiple users at once e.g. SELECT id, name FROM users WHERE id IN ($ids) 105 | ZIO.succeed(???) 106 | } 107 | 108 | result.foldCause( 109 | CompletedRequestMap.failCause(requests, _), 110 | CompletedRequestMap.fromIterableWith(_)(kv => GetUserName(kv._1), kv => Exit.succeed(kv._2)) 111 | ) 112 | } 113 | } 114 | 115 | def getUserNameById(id: Int): ZQuery[Any, Throwable, String] = 116 | ZQuery.fromRequest(GetUserName(id))(UserDataSource) 117 | 118 | val query: ZQuery[Any, Throwable, List[String]] = 119 | for { 120 | ids <- ZQuery.succeed(1 to 10) 121 | names <- ZQuery.foreachPar(ids)(id => getUserNameById(id)).map(_.toList) 122 | } yield (names) 123 | 124 | def run = query.run.tap(usernames => Console.printLine(s"Usernames: $usernames")) 125 | } 126 | ``` 127 | 128 | ## Resources 129 | 130 | - [Wicked Fast API Calls with ZIO Query](https://www.youtube.com/watch?v=rUUxDXJMzJo) by Adam Fraser (July 2020) (https://www.youtube.com/watch?v=rUUxDXJMzJo) 131 | -------------------------------------------------------------------------------- /zio-query/shared/src/main/scala/zio/query/internal/Result.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2023 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package zio.query.internal 18 | 19 | import zio._ 20 | import zio.query.internal.Result._ 21 | import zio.query.{DataSourceAspect, Described} 22 | import zio.stacktracer.TracingImplicits.disableAutoTrace 23 | 24 | /** 25 | * A `Result[R, E, A]` is the result of running one step of a `ZQuery`. A result 26 | * may either by done with a value `A`, blocked on a set of requests to data 27 | * sources that require an environment `R`, or failed with an `E`. 28 | */ 29 | private[query] sealed trait Result[-R, +E, +A] { self => 30 | 31 | /** 32 | * Folds over the successful or failed result. 33 | */ 34 | final def fold[B](failure: E => B, success: A => B)(implicit 35 | ev: CanFail[E], 36 | trace: Trace 37 | ): Result[R, Nothing, B] = 38 | self match { 39 | case Blocked(br, c) => blocked(br, c.fold(failure, success)) 40 | case Done(a) => done(success(a)) 41 | case Fail(e) => e.failureOrCause.fold(e => done(failure(e)), c => fail(c)) 42 | } 43 | 44 | /** 45 | * Maps the specified function over the successful value of this result. 46 | */ 47 | final def map[B](f: A => B)(implicit trace: Trace): Result[R, E, B] = 48 | self match { 49 | case Blocked(br, c) => blocked(br, c.map(f)) 50 | case Done(a) => done(f(a)) 51 | case Fail(e) => fail(e) 52 | } 53 | 54 | /** 55 | * Transforms all data sources with the specified data source aspect. 56 | */ 57 | def mapDataSources[R1 <: R](f: DataSourceAspect[R1])(implicit trace: Trace): Result[R1, E, A] = 58 | self match { 59 | case Blocked(br, c) => Result.blocked(br.mapDataSources(f), c.mapDataSources(f)) 60 | case Done(a) => Result.done(a) 61 | case Fail(e) => Result.fail(e) 62 | } 63 | 64 | /** 65 | * Maps the specified function over the failed value of this result. 66 | */ 67 | final def mapError[E1](f: E => E1)(implicit ev: CanFail[E], trace: Trace): Result[R, E1, A] = 68 | self match { 69 | case Blocked(br, c) => blocked(br, c.mapError(f)) 70 | case Done(a) => done(a) 71 | case Fail(e) => fail(e.map(f)) 72 | } 73 | 74 | /** 75 | * Maps the specified function over the failure cause of this result. 76 | */ 77 | def mapErrorCause[E1](f: Cause[E] => Cause[E1])(implicit trace: Trace): Result[R, E1, A] = 78 | self match { 79 | case Blocked(br, c) => blocked(br, c.mapErrorCause(f)) 80 | case Done(a) => done(a) 81 | case Fail(e) => fail(f(e)) 82 | } 83 | 84 | /** 85 | * Provides this result with its required environment. 86 | */ 87 | final def provideEnvironment( 88 | r: Described[ZEnvironment[R]] 89 | )(implicit trace: Trace): Result[Any, E, A] = 90 | provideSomeEnvironment(Described(_ => r.value, s"_ => ${r.description}")) 91 | 92 | /** 93 | * Provides this result with part of its required environment. 94 | */ 95 | final def provideSomeEnvironment[R0]( 96 | f: Described[ZEnvironment[R0] => ZEnvironment[R]] 97 | )(implicit trace: Trace): Result[R0, E, A] = 98 | self match { 99 | case Blocked(br, c) => blocked(br.provideSomeEnvironment(f), c.provideSomeEnvironment(f)) 100 | case Done(a) => done(a) 101 | case Fail(e) => fail(e) 102 | } 103 | } 104 | 105 | private[query] object Result { 106 | 107 | /** 108 | * Constructs a result that is blocked on the specified requests with the 109 | * specified continuation. 110 | */ 111 | def blocked[R, E, A](blockedRequests: BlockedRequests[R], continue: Continue[R, E, A]): Result[R, E, A] = 112 | Blocked(blockedRequests, continue) 113 | 114 | def blockedExit[R, E, A]( 115 | blockedRequests: BlockedRequests[R], 116 | continue: Continue[R, E, A] 117 | ): Exit[Nothing, Result[R, E, A]] = 118 | Exit.Success(Blocked(blockedRequests, continue)) 119 | 120 | /** 121 | * Constructs a result that is done with the specified value. 122 | */ 123 | def done[A](value: A): Result[Any, Nothing, A] = 124 | Done(value) 125 | 126 | def doneExit[A](value: A): Exit[Nothing, Result[Any, Nothing, A]] = 127 | Exit.Success(Done(value)) 128 | 129 | /** 130 | * Constructs a result that is failed with the specified `Cause`. 131 | */ 132 | def fail[E](cause: Cause[E]): Result[Any, E, Nothing] = 133 | Fail(cause) 134 | 135 | def failExit[E](cause: Cause[E]): Exit[Nothing, Result[Any, E, Nothing]] = 136 | Exit.Success(Fail(cause)) 137 | 138 | /** 139 | * Lifts an `Exit` into a result. 140 | */ 141 | def fromExit[E, A](exit: Exit[E, A]): Result[Any, E, A] = 142 | exit match { 143 | case Exit.Success(a) => Done(a) 144 | case Exit.Failure(e) => Fail(e) 145 | } 146 | 147 | val unit: Result[Any, Nothing, Unit] = done(()) 148 | 149 | final case class Blocked[-R, +E, +A](blockedRequests: BlockedRequests[R], continue: Continue[R, E, A]) 150 | extends Result[R, E, A] 151 | 152 | final case class Done[+A](value: A) extends Result[Any, Nothing, A] 153 | 154 | final case class Fail[+E](cause: Cause[E]) extends Result[Any, E, Nothing] 155 | } 156 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [//]: # (This file was autogenerated using `zio-sbt-website` plugin via `sbt generateReadme` command.) 2 | [//]: # (So please do not edit it manually. Instead, change "docs/index.md" file or sbt setting keys) 3 | [//]: # (e.g. "readmeDocumentation" and "readmeSupport".) 4 | 5 | # ZIO Query 6 | 7 | [ZIO Query](https://github.com/zio/zio-query) is a library for writing optimized queries to data sources in a high-level compositional style. It can add efficient pipelining, batching, and caching to any data source. ZIO Query helps us dramatically reduce load on data sources and improve performance. 8 | 9 | [![Production Ready](https://img.shields.io/badge/Project%20Stage-Production%20Ready-brightgreen.svg)](https://github.com/zio/zio/wiki/Project-Stages) ![CI Badge](https://github.com/zio/zio-query/workflows/CI/badge.svg) [![Sonatype Releases](https://img.shields.io/nexus/r/https/oss.sonatype.org/dev.zio/zio-query_2.13.svg?label=Sonatype%20Release)](https://oss.sonatype.org/content/repositories/releases/dev/zio/zio-query_2.13/) [![Sonatype Snapshots](https://img.shields.io/nexus/s/https/oss.sonatype.org/dev.zio/zio-query_2.13.svg?label=Sonatype%20Snapshot)](https://oss.sonatype.org/content/repositories/snapshots/dev/zio/zio-query_2.13/) [![javadoc](https://javadoc.io/badge2/dev.zio/zio-query-docs_2.13/javadoc.svg)](https://javadoc.io/doc/dev.zio/zio-query-docs_2.13) [![ZIO Query](https://img.shields.io/github/stars/zio/zio-query?style=social)](https://github.com/zio/zio-query) 10 | 11 | ## Introduction 12 | 13 | Some key features of ZIO Query: 14 | 15 | - **Batching** — ZIO Query detects parts of composite queries that can be executed in parallel without changing the semantics of the query. 16 | - **Pipelining** — ZIO Query detects parts of composite queries that can be combined together for fewer individual requests to the data source. 17 | - **Caching** — ZIO Query can transparently cache read queries to minimize the cost of fetching the same item repeatedly in the scope of a query. 18 | 19 | Compared with Fetch, ZIO Query supports response types that depend on request types, does not require higher-kinded types and implicits, supports ZIO environment and statically typed errors, and has no dependencies except for ZIO. 20 | 21 | A `ZQuery[R, E, A]` is a purely functional description of an effectual query that may contain requests from one or more data sources, requires an environment `R`, and may fail with an `E` or succeed with an `A`. 22 | 23 | Requests that can be performed in parallel, as expressed by `zipWithPar` and combinators derived from it, will automatically be batched. Requests that must be performed sequentially, as expressed by `zipWith` and combinators derived from it, will automatically be pipelined. This allows for aggressive data source specific optimizations. Requests can also be deduplicated and cached. 24 | 25 | 26 | This allows for writing queries in a high level, compositional style, with confidence that they will automatically be optimized. For example, consider the following query from a user service. 27 | 28 | Assume we have the following database access layer APIs: 29 | 30 | ```scala 31 | def getAllUserIds: ZIO[Any, Nothing, List[Int]] = { 32 | // Get all user IDs e.g. SELECT id FROM users 33 | ZIO.succeed(???) 34 | } 35 | 36 | def getUserNameById(id: Int): ZIO[Any, Nothing, String] = { 37 | // Get user by ID e.g. SELECT name FROM users WHERE id = $id 38 | ZIO.succeed(???) 39 | } 40 | ``` 41 | 42 | We can get their corresponding usernames from the database by the following code snippet: 43 | 44 | ```scala 45 | val userNames = for { 46 | ids <- getAllUserIds 47 | names <- ZIO.foreachPar(ids)(getUserNameById) 48 | } yield names 49 | ``` 50 | 51 | It works, but this is not performant. It is going to query the underlying database _N + 1_ times, one for `getAllUserIds` and one for each call to `getUserNameById`. 52 | 53 | In contrast, `ZQuery` will automatically optimize this to two queries, one for `userIds` and one for `userNames`: 54 | 55 | ```scala 56 | lazy val getAllUserIds: ZQuery[Any, Nothing, List[Int]] = ??? 57 | def getUserNameById(id: Int): ZQuery[Any, Nothing, String] = ??? 58 | 59 | lazy val userQuery: ZQuery[Any, Nothing, List[String]] = for { 60 | userIds <- getAllUserIds 61 | userNames <- ZQuery.foreachPar(userIds)(getUserNameById) 62 | } yield userNames 63 | ``` 64 | 65 | ## Installation 66 | 67 | In order to use this library, we need to add the following line in our `build.sbt` file: 68 | 69 | ```scala 70 | libraryDependencies += "dev.zio" %% "zio-query" % "0.7.6" 71 | ``` 72 | 73 | ## Example 74 | 75 | Here is an example of using ZIO Query, which optimizes multiple database queries by batching all of them in one query: 76 | 77 | ```scala 78 | import zio._ 79 | import zio.query._ 80 | 81 | object ZQueryExample extends ZIOAppDefault { 82 | case class GetUserName(id: Int) extends Request[Throwable, String] 83 | 84 | lazy val UserDataSource: DataSource.Batched[Any, GetUserName] = 85 | new DataSource.Batched[Any, GetUserName] { 86 | val identifier: String = "UserDataSource" 87 | 88 | def run(requests: Chunk[GetUserName])(implicit trace: Trace): ZIO[Any, Nothing, CompletedRequestMap] = 89 | requests.toList match { 90 | case request :: Nil => 91 | val result: Task[String] = { 92 | // get user by ID e.g. SELECT name FROM users WHERE id = $id 93 | ZIO.succeed(???) 94 | } 95 | 96 | result.exit.map(CompletedRequestMap.single(request, _)) 97 | 98 | case batch: Seq[GetUserName] => 99 | val result: Task[List[(Int, String)]] = { 100 | // get multiple users at once e.g. SELECT id, name FROM users WHERE id IN ($ids) 101 | ZIO.succeed(???) 102 | } 103 | 104 | result.foldCause( 105 | CompletedRequestMap.failCause(requests, _), 106 | CompletedRequestMap.fromIterableWith(_)(kv => GetUserName(kv._1), kv => Exit.succeed(kv._2)) 107 | ) 108 | } 109 | } 110 | 111 | def getUserNameById(id: Int): ZQuery[Any, Throwable, String] = 112 | ZQuery.fromRequest(GetUserName(id))(UserDataSource) 113 | 114 | val query: ZQuery[Any, Throwable, List[String]] = 115 | for { 116 | ids <- ZQuery.succeed(1 to 10) 117 | names <- ZQuery.foreachPar(ids)(id => getUserNameById(id)).map(_.toList) 118 | } yield (names) 119 | 120 | def run = query.run.tap(usernames => Console.printLine(s"Usernames: $usernames")) 121 | } 122 | ``` 123 | 124 | ## Resources 125 | 126 | - [Wicked Fast API Calls with ZIO Query](https://www.youtube.com/watch?v=rUUxDXJMzJo) by Adam Fraser (July 2020) (https://www.youtube.com/watch?v=rUUxDXJMzJo) 127 | 128 | ## Documentation 129 | 130 | Learn more on the [ZIO Query homepage](https://zio.dev/zio-query)! 131 | 132 | ## Contributing 133 | 134 | For the general guidelines, see ZIO [contributor's guide](https://zio.dev/contributor-guidelines). 135 | 136 | ## Code of Conduct 137 | 138 | See the [Code of Conduct](https://zio.dev/code-of-conduct) 139 | 140 | ## Support 141 | 142 | Come chat with us on [![Badge-Discord]][Link-Discord]. 143 | 144 | [Badge-Discord]: https://img.shields.io/discord/629491597070827530?logo=discord "chat on discord" 145 | [Link-Discord]: https://discord.gg/2ccFBr4 "Discord" 146 | 147 | ## License 148 | 149 | [License](LICENSE) 150 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dropbox settings and caches 2 | .dropbox 3 | .dropbox.attr 4 | .dropbox.cache 5 | # -*- mode: gitignore; -*- 6 | *~ 7 | \#*\# 8 | /.emacs.desktop 9 | /.emacs.desktop.lock 10 | *.elc 11 | auto-save-list 12 | tramp 13 | .\#* 14 | 15 | # Org-mode 16 | .org-id-locations 17 | *_archive 18 | 19 | # flymake-mode 20 | *_flymake.* 21 | 22 | # eshell files 23 | /eshell/history 24 | /eshell/lastdir 25 | 26 | # elpa packages 27 | /elpa/ 28 | 29 | # reftex files 30 | *.rel 31 | 32 | # AUCTeX auto folder 33 | /auto/ 34 | 35 | # cask packages 36 | .cask/ 37 | dist/ 38 | 39 | # Flycheck 40 | flycheck_*.el 41 | 42 | # server auth directory 43 | /server/ 44 | 45 | # projectiles files 46 | .projectile 47 | 48 | # directory configuration 49 | .dir-locals.el 50 | *~ 51 | 52 | # temporary files which can be created if a process still has a handle open of a deleted file 53 | .fuse_hidden* 54 | 55 | # KDE directory preferences 56 | .directory 57 | 58 | # Linux trash folder which might appear on any partition or disk 59 | .Trash-* 60 | 61 | # .nfs files are created when an open file is removed but is still being accessed 62 | .nfs* 63 | # General 64 | .DS_Store 65 | .AppleDouble 66 | .LSOverride 67 | 68 | # Icon must end with two \r 69 | Icon 70 | 71 | # Thumbnails 72 | ._* 73 | 74 | # Files that might appear in the root of a volume 75 | .DocumentRevisions-V100 76 | .fseventsd 77 | .Spotlight-V100 78 | .TemporaryItems 79 | .Trashes 80 | .VolumeIcon.icns 81 | .com.apple.timemachine.donotpresent 82 | 83 | # Directories potentially created on remote AFP share 84 | .AppleDB 85 | .AppleDesktop 86 | Network Trash Folder 87 | Temporary Items 88 | .apdisk 89 | # Cache files for Sublime Text 90 | *.tmlanguage.cache 91 | *.tmPreferences.cache 92 | *.stTheme.cache 93 | 94 | # Workspace files are user-specific 95 | *.sublime-workspace 96 | 97 | # Project files should be checked into the repository, unless a significant 98 | # proportion of contributors will probably not be using Sublime Text 99 | # *.sublime-project 100 | 101 | # SFTP configuration file 102 | sftp-config.json 103 | 104 | # Package control specific files 105 | Package Control.last-run 106 | Package Control.ca-list 107 | Package Control.ca-bundle 108 | Package Control.system-ca-bundle 109 | Package Control.cache/ 110 | Package Control.ca-certs/ 111 | Package Control.merged-ca-bundle 112 | Package Control.user-ca-bundle 113 | oscrypto-ca-bundle.crt 114 | bh_unicode_properties.cache 115 | 116 | # Sublime-github package stores a github token in this file 117 | # https://packagecontrol.io/packages/sublime-github 118 | GitHub.sublime-settings 119 | # Ignore tags created by etags, ctags, gtags (GNU global) and cscope 120 | TAGS 121 | .TAGS 122 | !TAGS/ 123 | tags 124 | .tags 125 | !tags/ 126 | gtags.files 127 | GTAGS 128 | GRTAGS 129 | GPATH 130 | GSYMS 131 | cscope.files 132 | cscope.out 133 | cscope.in.out 134 | cscope.po.out 135 | 136 | *.tmproj 137 | *.tmproject 138 | tmtags 139 | # Swap 140 | [._]*.s[a-v][a-z] 141 | [._]*.sw[a-p] 142 | [._]s[a-rt-v][a-z] 143 | [._]ss[a-gi-z] 144 | [._]sw[a-p] 145 | 146 | # Session 147 | Session.vim 148 | 149 | # Temporary 150 | .netrwhist 151 | *~ 152 | # Auto-generated tag files 153 | tags 154 | # Persistent undo 155 | [._]*.un~ 156 | # Windows thumbnail cache files 157 | Thumbs.db 158 | ehthumbs.db 159 | ehthumbs_vista.db 160 | 161 | # Dump file 162 | *.stackdump 163 | 164 | # Folder config file 165 | [Dd]esktop.ini 166 | 167 | # Recycle Bin used on file shares 168 | $RECYCLE.BIN/ 169 | 170 | # Windows Installer files 171 | *.cab 172 | *.msi 173 | *.msix 174 | *.msm 175 | *.msp 176 | 177 | # Windows shortcuts 178 | *.lnk 179 | 180 | .metadata 181 | bin/ 182 | tmp/ 183 | *.tmp 184 | *.bak 185 | *.swp 186 | *~.nib 187 | local.properties 188 | .settings/ 189 | .loadpath 190 | .recommenders 191 | 192 | # External tool builders 193 | .externalToolBuilders/ 194 | 195 | # Locally stored "Eclipse launch configurations" 196 | *.launch 197 | 198 | # PyDev specific (Python IDE for Eclipse) 199 | *.pydevproject 200 | 201 | # CDT-specific (C/C++ Development Tooling) 202 | .cproject 203 | 204 | # CDT- autotools 205 | .autotools 206 | 207 | # Java annotation processor (APT) 208 | .factorypath 209 | 210 | # PDT-specific (PHP Development Tools) 211 | .buildpath 212 | 213 | # sbteclipse plugin 214 | .target 215 | 216 | # Tern plugin 217 | .tern-project 218 | 219 | # TeXlipse plugin 220 | .texlipse 221 | 222 | # STS (Spring Tool Suite) 223 | .springBeans 224 | 225 | # Code Recommenders 226 | .recommenders/ 227 | 228 | # Annotation Processing 229 | .apt_generated/ 230 | 231 | # Scala IDE specific (Scala & Java development for Eclipse) 232 | .cache-main 233 | .scala_dependencies 234 | .worksheet 235 | # Ensime specific 236 | .ensime 237 | .ensime_cache/ 238 | .ensime_lucene/ 239 | # default application storage directory used by the IDE Performance Cache feature 240 | .data/ 241 | 242 | # used for ADF styles caching 243 | temp/ 244 | 245 | # default output directories 246 | classes/ 247 | deploy/ 248 | javadoc/ 249 | 250 | # lock file, a part of Oracle Credential Store Framework 251 | cwallet.sso.lck# JEnv local Java version configuration file 252 | .java-version 253 | 254 | # Used by previous versions of JEnv 255 | .jenv-version 256 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 257 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 258 | 259 | # CMake 260 | cmake-build-*/ 261 | 262 | # File-based project format 263 | *.iws 264 | 265 | # IntelliJ 266 | out/ 267 | 268 | # mpeltonen/sbt-idea plugin 269 | .idea_modules/ 270 | 271 | # JIRA plugin 272 | atlassian-ide-plugin.xml 273 | 274 | # Crashlytics plugin (for Android Studio and IntelliJ) 275 | com_crashlytics_export_strings.xml 276 | crashlytics.properties 277 | crashlytics-build.properties 278 | fabric.properties 279 | 280 | # Editor-based Rest Client 281 | nbproject/private/ 282 | build/ 283 | nbbuild/ 284 | dist/ 285 | nbdist/ 286 | .nb-gradle/ 287 | # Built application files 288 | *.apk 289 | *.ap_ 290 | 291 | # Files for the ART/Dalvik VM 292 | *.dex 293 | 294 | # Java class files 295 | *.class 296 | 297 | # Generated files 298 | bin/ 299 | gen/ 300 | out/ 301 | 302 | # Gradle files 303 | .gradle/ 304 | build/ 305 | 306 | # Local configuration file (sdk path, etc) 307 | local.properties 308 | 309 | # Proguard folder generated by Eclipse 310 | proguard/ 311 | 312 | # Log Files 313 | *.log 314 | 315 | # Android Studio Navigation editor temp files 316 | .navigation/ 317 | 318 | # Android Studio captures folder 319 | captures/ 320 | 321 | # IntelliJ 322 | *.iml 323 | .idea 324 | 325 | # Keystore files 326 | # Uncomment the following line if you do not want to check your keystore files in. 327 | #*.jks 328 | 329 | # External native build folder generated in Android Studio 2.2 and later 330 | .externalNativeBuild 331 | 332 | # Google Services (e.g. APIs or Firebase) 333 | google-services.json 334 | 335 | # Freeline 336 | freeline.py 337 | freeline/ 338 | freeline_project_description.json 339 | 340 | # fastlane 341 | fastlane/report.xml 342 | fastlane/Preview.html 343 | fastlane/screenshots 344 | fastlane/test_output 345 | fastlane/readme.md 346 | .gradle 347 | /build/ 348 | 349 | # Ignore Gradle GUI config 350 | gradle-app.setting 351 | 352 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 353 | !gradle-wrapper.jar 354 | 355 | # Cache of project 356 | .gradletasknamecache 357 | 358 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 359 | # gradle/wrapper/gradle-wrapper.properties 360 | # Compiled class file 361 | *.class 362 | 363 | # Log file 364 | *.log 365 | 366 | # BlueJ files 367 | *.ctxt 368 | 369 | # Mobile Tools for Java (J2ME) 370 | .mtj.tmp/ 371 | 372 | # Package Files # 373 | *.jar 374 | *.war 375 | *.nar 376 | *.ear 377 | *.zip 378 | *.tar.gz 379 | *.rar 380 | 381 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 382 | hs_err_pid* 383 | target/ 384 | pom.xml.tag 385 | pom.xml.releaseBackup 386 | pom.xml.versionsBackup 387 | pom.xml.next 388 | release.properties 389 | dependency-reduced-pom.xml 390 | buildNumber.properties 391 | .mvn/timing.properties 392 | .mvn/wrapper/maven-wrapper.jar 393 | # Simple Build Tool 394 | # http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control 395 | 396 | dist/* 397 | target/ 398 | lib_managed/ 399 | src_managed/ 400 | project/boot/ 401 | project/plugins/project/ 402 | .history 403 | .cache 404 | .lib/ 405 | *.class 406 | *.log 407 | 408 | .metals/ 409 | project/metals.sbt 410 | .bloop/ 411 | project/secret 412 | 413 | # mdoc 414 | website/node_modules 415 | website/build 416 | website/i18n/en.json 417 | website/static/api 418 | .bsp -------------------------------------------------------------------------------- /zio-query/shared/src/main/scala/zio/query/internal/Continue.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2023 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package zio.query.internal 18 | 19 | import zio._ 20 | import zio.query._ 21 | import zio.query.internal.Continue._ 22 | import zio.stacktracer.TracingImplicits.disableAutoTrace 23 | 24 | /** 25 | * A `Continue[R, E, A]` models a continuation of a blocked request that 26 | * requires an environment `R` and may either fail with an `E` or succeed with 27 | * an `A`. A continuation may either be a `Get` that merely gets the result of a 28 | * blocked request (potentially transforming it with pure functions) or an 29 | * `Effect` that may perform arbitrary effects. This is used by the library 30 | * internally to determine whether it is safe to pipeline two requests that must 31 | * be executed sequentially. 32 | */ 33 | private[query] sealed trait Continue[-R, +E, +A] { self => 34 | 35 | /** 36 | * Purely folds over the failure and success types of this continuation. 37 | */ 38 | final def fold[B](failure: E => B, success: A => B)(implicit 39 | ev: CanFail[E], 40 | trace: Trace 41 | ): Continue[R, Nothing, B] = 42 | self match { 43 | case Effect(query) => Effect(query.fold(failure, success)) 44 | case Get(io) => Get(io.foldZIO(e => Exit.succeed(failure(e)), a => Exit.succeed(success(a)))) 45 | } 46 | 47 | /** 48 | * Effectually folds over the failure and success types of this continuation. 49 | */ 50 | final def foldCauseQuery[R1 <: R, E1, B]( 51 | failure: Cause[E] => ZQuery[R1, E1, B], 52 | success: A => ZQuery[R1, E1, B] 53 | )(implicit trace: Trace): Continue[R1, E1, B] = 54 | self match { 55 | case Effect(query) => Effect(query.foldCauseQuery(failure, success)) 56 | case Get(io) => Effect(ZQuery.fromZIONow(io).foldCauseQuery(failure, success)) 57 | } 58 | 59 | final def foldCauseZIO[R1 <: R, E1, B]( 60 | failure: Cause[E] => ZIO[R1, E1, B], 61 | success: A => ZIO[R1, E1, B] 62 | )(implicit trace: Trace): Continue[R1, E1, B] = 63 | self match { 64 | case Effect(query) => Effect(query.foldCauseZIO(failure, success)) 65 | case Get(io) => Get(io.foldCauseZIO(failure, success)) 66 | } 67 | 68 | final def foldZIO[R1 <: R, E1, B]( 69 | failure: E => ZIO[R1, E1, B], 70 | success: A => ZIO[R1, E1, B] 71 | )(implicit trace: Trace): Continue[R1, E1, B] = 72 | foldCauseZIO(_.failureOrCause.fold(failure, Exit.failCause), success) 73 | 74 | /** 75 | * Purely maps over the success type of this continuation. 76 | */ 77 | final def map[B](f: A => B)(implicit trace: Trace): Continue[R, E, B] = 78 | self match { 79 | case Effect(query) => Effect(query.map(f)) 80 | case Get(io) => Get(io.map(f)) 81 | } 82 | 83 | final def mapBothCause[E1, B](failure: Cause[E] => Cause[E1], success: A => B)(implicit 84 | ev: CanFail[E], 85 | trace: Trace 86 | ): Continue[R, E1, B] = 87 | self match { 88 | case Effect(query) => Effect(query.mapBothCause(failure, success)) 89 | case Get(io) => Get(io.foldCauseZIO(e => Exit.failCause(failure(e)), a => Exit.succeed(success(a)))) 90 | } 91 | 92 | /** 93 | * Transforms all data sources with the specified data source aspect. 94 | */ 95 | final def mapDataSources[R1 <: R](f: DataSourceAspect[R1])(implicit trace: Trace): Continue[R1, E, A] = 96 | self match { 97 | case Effect(query) => Effect(query.mapDataSources(f)) 98 | case Get(io) => Get(io) 99 | } 100 | 101 | /** 102 | * Purely maps over the failure type of this continuation. 103 | */ 104 | final def mapError[E1](f: E => E1)(implicit ev: CanFail[E], trace: Trace): Continue[R, E1, A] = 105 | self match { 106 | case Effect(query) => Effect(query.mapError(f)) 107 | case Get(io) => Get(io.mapError(f)) 108 | } 109 | 110 | /** 111 | * Purely maps over the failure cause of this continuation. 112 | */ 113 | final def mapErrorCause[E1](f: Cause[E] => Cause[E1])(implicit trace: Trace): Continue[R, E1, A] = 114 | self match { 115 | case Effect(query) => Effect(query.mapErrorCause(f)) 116 | case Get(io) => Get(io.mapErrorCause(f)) 117 | } 118 | 119 | /** 120 | * Effectually maps over the success type of this continuation. 121 | */ 122 | final def mapQuery[R1 <: R, E1 >: E, B]( 123 | f: A => ZQuery[R1, E1, B] 124 | )(implicit trace: Trace): Continue[R1, E1, B] = 125 | self match { 126 | case Effect(query) => Effect(query.flatMap(f)) 127 | case Get(io) => Effect(ZQuery.fromZIONow(io).flatMap(f)) 128 | } 129 | 130 | /** 131 | * Effectually maps over the success type of this continuation. 132 | */ 133 | final def mapZIO[R1 <: R, E1 >: E, B]( 134 | f: A => ZIO[R1, E1, B] 135 | )(implicit trace: Trace): Continue[R1, E1, B] = 136 | self match { 137 | case Effect(query) => Effect(query.mapZIO(f)) 138 | case Get(io) => Get(io.flatMap(f)) 139 | } 140 | 141 | /** 142 | * Purely contramaps over the environment type of this continuation. 143 | */ 144 | final def provideSomeEnvironment[R0]( 145 | f: Described[ZEnvironment[R0] => ZEnvironment[R]] 146 | )(implicit trace: Trace): Continue[R0, E, A] = 147 | self match { 148 | case Effect(query) => Effect(query.provideSomeEnvironment(f)) 149 | case Get(io) => Get(io.provideSomeEnvironment(f.value)) 150 | } 151 | 152 | /** 153 | * Combines this continuation with that continuation using the specified 154 | * function, in sequence. 155 | */ 156 | final def zipWith[R1 <: R, E1 >: E, B, C]( 157 | that: Continue[R1, E1, B] 158 | )(f: (A, B) => C)(implicit trace: Trace): Continue[R1, E1, C] = 159 | (self, that) match { 160 | case (Effect(l), Effect(r)) => Effect(l.zipWith(r)(f)) 161 | case (Effect(l), Get(r)) => Effect(l.zipWith(ZQuery.fromZIONow(r))(f)) 162 | case (Get(l), Effect(r)) => Effect(ZQuery.fromZIONow(l).zipWith(r)(f)) 163 | case (Get(l), Get(r)) => Get(l.zipWith(r)(f)) 164 | } 165 | 166 | /** 167 | * Combines this continuation with that continuation using the specified 168 | * function, in parallel. 169 | */ 170 | final def zipWithPar[R1 <: R, E1 >: E, B, C]( 171 | that: Continue[R1, E1, B] 172 | )(f: (A, B) => C)(implicit trace: Trace): Continue[R1, E1, C] = 173 | (self, that) match { 174 | case (Effect(l), Effect(r)) => Effect(l.zipWithPar(r)(f)) 175 | case (Effect(l), Get(r)) => Effect(l.zipWith(ZQuery.fromZIONow(r))(f)) 176 | case (Get(l), Effect(r)) => Effect(ZQuery.fromZIONow(l).zipWith(r)(f)) 177 | case (Get(l), Get(r)) => Get(l.zipWith(r)(f)) 178 | } 179 | 180 | /** 181 | * Combines this continuation with that continuation using the specified 182 | * function, batching requests to data sources. 183 | */ 184 | final def zipWithBatched[R1 <: R, E1 >: E, B, C]( 185 | that: Continue[R1, E1, B] 186 | )(f: (A, B) => C)(implicit trace: Trace): Continue[R1, E1, C] = 187 | (self, that) match { 188 | case (Effect(l), Effect(r)) => Effect(l.zipWithBatched(r)(f)) 189 | case (Effect(l), Get(r)) => Effect(l.zipWith(ZQuery.fromZIONow(r))(f)) 190 | case (Get(l), Effect(r)) => Effect(ZQuery.fromZIONow(l).zipWith(r)(f)) 191 | case (Get(l), Get(r)) => Get(l.zipWith(r)(f)) 192 | } 193 | } 194 | 195 | private[query] object Continue { 196 | 197 | /** 198 | * Constructs a continuation from a request, a data source, and a `Promise` 199 | * that will contain the result of the request when it is executed. 200 | */ 201 | def apply[E, A](promise: Promise[E, A])(implicit trace: Trace): Continue[Any, E, A] = 202 | Get(promise.await) 203 | 204 | /** 205 | * Constructs a continuation that may perform arbitrary effects. 206 | */ 207 | def effect[R, E, A](query: ZQuery[R, E, A]): Continue[R, E, A] = 208 | Effect(query) 209 | 210 | /** 211 | * Constructs a continuation that merely gets the result of a blocked request 212 | * (potentially transforming it with pure functions). 213 | */ 214 | def get[R, E, A](io: ZIO[R, E, A]): Continue[R, E, A] = 215 | Get(io) 216 | 217 | final case class Effect[R, E, A](query: ZQuery[R, E, A]) extends Continue[R, E, A] 218 | final case class Get[R, E, A](io: ZIO[R, E, A]) extends Continue[R, E, A] 219 | } 220 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file was autogenerated using `zio-sbt-ci` plugin via `sbt ciGenerateGithubWorkflow` 2 | # task and should be included in the git repository. Please do not edit it manually. 3 | 4 | name: CI 5 | env: 6 | JDK_JAVA_OPTIONS: -XX:+PrintCommandLineFlags 7 | 'on': 8 | workflow_dispatch: {} 9 | release: 10 | types: 11 | - published 12 | push: 13 | branches: 14 | - series/2.x 15 | pull_request: 16 | branches-ignore: 17 | - gh-pages 18 | concurrency: 19 | group: ${{ github.workflow }}-${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.run_id || github.ref }} 20 | cancel-in-progress: true 21 | jobs: 22 | build: 23 | name: Build 24 | runs-on: ubuntu-latest 25 | continue-on-error: true 26 | env: 27 | CI_RELEASE_MODE: '1' 28 | steps: 29 | - name: Git Checkout 30 | uses: actions/checkout@v4 31 | with: 32 | fetch-depth: '0' 33 | - name: Setup Scala 34 | uses: actions/setup-java@v4 35 | with: 36 | distribution: corretto 37 | java-version: '17' 38 | check-latest: true 39 | - name: Setup sbt 40 | uses: sbt/setup-sbt@v1 41 | - name: Cache Dependencies 42 | uses: coursier/cache-action@v6 43 | - name: Check all code compiles 44 | run: sbt --client +Test/compile 45 | - name: Check artifacts build process 46 | run: sbt --client +publishLocal 47 | - name: Check binary compatibility 48 | run: sbt --client "+zioQueryJVM/mimaReportBinaryIssues; +zioQueryJS/mimaReportBinaryIssues" 49 | build-website: 50 | name: Build Website 51 | runs-on: ubuntu-latest 52 | continue-on-error: true 53 | steps: 54 | - name: Git Checkout 55 | uses: actions/checkout@v4 56 | with: 57 | fetch-depth: '0' 58 | - name: Setup Scala 59 | uses: actions/setup-java@v4 60 | with: 61 | distribution: corretto 62 | java-version: '17' 63 | check-latest: true 64 | - name: Setup sbt 65 | uses: sbt/setup-sbt@v1 66 | - name: Cache Dependencies 67 | uses: coursier/cache-action@v6 68 | - name: Check website build process 69 | run: sbt docs/buildWebsite 70 | lint: 71 | name: Lint 72 | runs-on: ubuntu-latest 73 | continue-on-error: false 74 | steps: 75 | - name: Git Checkout 76 | uses: actions/checkout@v4 77 | with: 78 | fetch-depth: '0' 79 | - name: Setup Scala 80 | uses: actions/setup-java@v4 81 | with: 82 | distribution: corretto 83 | java-version: '17' 84 | check-latest: true 85 | - name: Setup sbt 86 | uses: sbt/setup-sbt@v1 87 | - name: Cache Dependencies 88 | uses: coursier/cache-action@v6 89 | - name: Lint 90 | run: sbt lint 91 | test: 92 | name: Test 93 | runs-on: ubuntu-latest 94 | continue-on-error: false 95 | strategy: 96 | fail-fast: false 97 | matrix: 98 | java: 99 | - '11' 100 | - '21' 101 | scala-project: 102 | - ++2.12 zioQueryJVM 103 | - ++2.13 zioQueryJVM 104 | - ++3.3 zioQueryJVM 105 | - ++2.13 zioQueryJS 106 | - ++3.3 zioQueryJS 107 | - ++2.13 zioQueryNative 108 | - ++3.3 zioQueryNative 109 | steps: 110 | - name: Setup Scala 111 | uses: actions/setup-java@v4 112 | with: 113 | distribution: corretto 114 | java-version: ${{ matrix.java }} 115 | check-latest: true 116 | - name: Setup sbt 117 | uses: sbt/setup-sbt@v1 118 | - name: Cache Dependencies 119 | uses: coursier/cache-action@v6 120 | - name: Git Checkout 121 | uses: actions/checkout@v4 122 | with: 123 | fetch-depth: '0' 124 | - name: Test 125 | run: sbt ${{ matrix.scala-project }}/test 126 | update-readme: 127 | name: Update README 128 | runs-on: ubuntu-latest 129 | continue-on-error: false 130 | if: ${{ github.event_name == 'push' }} 131 | steps: 132 | - name: Git Checkout 133 | uses: actions/checkout@v4 134 | with: 135 | fetch-depth: '0' 136 | - name: Setup Scala 137 | uses: actions/setup-java@v4 138 | with: 139 | distribution: corretto 140 | java-version: '17' 141 | check-latest: true 142 | - name: Setup sbt 143 | uses: sbt/setup-sbt@v1 144 | - name: Cache Dependencies 145 | uses: coursier/cache-action@v6 146 | - name: Generate Readme 147 | run: sbt docs/generateReadme 148 | - name: Commit Changes 149 | run: | 150 | git config --local user.email "zio-assistant[bot]@users.noreply.github.com" 151 | git config --local user.name "ZIO Assistant" 152 | git add README.md 153 | git commit -m "Update README.md" || echo "No changes to commit" 154 | - name: Generate Token 155 | id: generate-token 156 | uses: zio/generate-github-app-token@v1.0.0 157 | with: 158 | app_id: ${{ secrets.APP_ID }} 159 | app_private_key: ${{ secrets.APP_PRIVATE_KEY }} 160 | - name: Create Pull Request 161 | id: cpr 162 | uses: peter-evans/create-pull-request@v6 163 | with: 164 | body: |- 165 | Autogenerated changes after running the `sbt docs/generateReadme` command of the [zio-sbt-website](https://zio.dev/zio-sbt) plugin. 166 | 167 | I will automatically update the README.md file whenever there is new change for README.md, e.g. 168 | - After each release, I will update the version in the installation section. 169 | - After any changes to the "docs/index.md" file, I will update the README.md file accordingly. 170 | branch: zio-sbt-website/update-readme 171 | commit-message: Update README.md 172 | token: ${{ steps.generate-token.outputs.token }} 173 | delete-branch: true 174 | title: Update README.md 175 | - name: Approve PR 176 | if: ${{ steps.cpr.outputs.pull-request-number }} 177 | run: gh pr review "$PR_URL" --approve 178 | env: 179 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 180 | PR_URL: ${{ steps.cpr.outputs.pull-request-url }} 181 | - name: Enable Auto-Merge 182 | if: ${{ steps.cpr.outputs.pull-request-number }} 183 | run: gh pr merge --auto --squash "$PR_URL" || gh pr merge --squash "$PR_URL" 184 | env: 185 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 186 | PR_URL: ${{ steps.cpr.outputs.pull-request-url }} 187 | ci: 188 | name: ci 189 | runs-on: ubuntu-latest 190 | continue-on-error: false 191 | needs: 192 | - lint 193 | - test 194 | - build 195 | - build-website 196 | steps: 197 | - name: Report Successful CI 198 | run: echo "ci passed" 199 | release: 200 | name: Release 201 | runs-on: ubuntu-latest 202 | continue-on-error: false 203 | needs: 204 | - ci 205 | if: ${{ github.event_name != 'pull_request' }} 206 | steps: 207 | - name: Git Checkout 208 | uses: actions/checkout@v4 209 | with: 210 | fetch-depth: '0' 211 | - name: Setup Scala 212 | uses: actions/setup-java@v4 213 | with: 214 | distribution: corretto 215 | java-version: '17' 216 | check-latest: true 217 | - name: Setup sbt 218 | uses: sbt/setup-sbt@v1 219 | - name: Cache Dependencies 220 | uses: coursier/cache-action@v6 221 | - name: Release 222 | run: sbt ci-release 223 | env: 224 | CI_RELEASE_MODE: '1' 225 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 226 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 227 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 228 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 229 | release-docs: 230 | name: Release Docs 231 | runs-on: ubuntu-latest 232 | continue-on-error: false 233 | needs: 234 | - release 235 | if: ${{ ((github.event_name == 'release') && (github.event.action == 'published')) || (github.event_name == 'workflow_dispatch') }} 236 | steps: 237 | - name: Git Checkout 238 | uses: actions/checkout@v4 239 | with: 240 | fetch-depth: '0' 241 | - name: Setup Scala 242 | uses: actions/setup-java@v4 243 | with: 244 | distribution: corretto 245 | java-version: '17' 246 | check-latest: true 247 | - name: Setup sbt 248 | uses: sbt/setup-sbt@v1 249 | - name: Cache Dependencies 250 | uses: coursier/cache-action@v6 251 | - name: Setup NodeJs 252 | uses: actions/setup-node@v4 253 | with: 254 | node-version: 16.x 255 | registry-url: https://registry.npmjs.org 256 | - name: Publish Docs to NPM Registry 257 | run: sbt docs/publishToNpm 258 | env: 259 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 260 | notify-docs-release: 261 | name: Notify Docs Release 262 | runs-on: ubuntu-latest 263 | continue-on-error: false 264 | needs: 265 | - release-docs 266 | if: ${{ (github.event_name == 'release') && (github.event.action == 'published') }} 267 | steps: 268 | - name: Git Checkout 269 | uses: actions/checkout@v4 270 | with: 271 | fetch-depth: '0' 272 | - name: notify the main repo about the new release of docs package 273 | run: | 274 | PACKAGE_NAME=$(cat docs/package.json | grep '"name"' | awk -F'"' '{print $4}') 275 | PACKAGE_VERSION=$(npm view $PACKAGE_NAME version) 276 | curl -L \ 277 | -X POST \ 278 | -H "Accept: application/vnd.github+json" \ 279 | -H "Authorization: token ${{ secrets.PAT_TOKEN }}"\ 280 | https://api.github.com/repos/zio/zio/dispatches \ 281 | -d '{ 282 | "event_type":"update-docs", 283 | "client_payload":{ 284 | "package_name":"'"${PACKAGE_NAME}"'", 285 | "package_version": "'"${PACKAGE_VERSION}"'" 286 | } 287 | }' 288 | -------------------------------------------------------------------------------- /zio-query/shared/src/main/scala/zio/query/CompletedRequestMap.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2023 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package zio.query 18 | 19 | import zio.stacktracer.TracingImplicits.disableAutoTrace 20 | import zio.{Cause, Chunk, Exit} 21 | 22 | import scala.collection.compat._ 23 | import scala.collection.{immutable, mutable} 24 | 25 | /** 26 | * A `CompletedRequestMap` is a universally quantified mapping from requests of 27 | * type `Request[E, A]` to results of type `Exit[E, A]` for all types `E` and 28 | * `A`. The guarantee is that for any request of type `Request[E, A]`, if there 29 | * is a corresponding value in the map, that value is of type `Exit[E, A]`. This 30 | * is used by the library to support data sources that return different result 31 | * types for different requests while guaranteeing that results will be of the 32 | * type requested. 33 | */ 34 | 35 | sealed abstract class CompletedRequestMap { self => 36 | 37 | protected val map: collection.Map[Any, Exit[Any, Any]] 38 | 39 | /** 40 | * Appends the specified result to the completed requests map. 41 | * 42 | * @deprecated 43 | * Usage of this method is deprecated as it leads to performance 44 | * degradation. Prefer using one of the constructors in the companion object 45 | * instead 46 | */ 47 | @deprecated("Use one of the constructors in the companion object instead. See scaladoc for more info", "0.7.0") 48 | final def ++(that: CompletedRequestMap): CompletedRequestMap = 49 | new CompletedRequestMap.Immutable(immutable.HashMap.from(map) ++ that.map) 50 | 51 | /** 52 | * Returns whether a result exists for the specified request. 53 | */ 54 | final def contains(request: Any): Boolean = 55 | map.contains(request) 56 | 57 | /** 58 | * Whether the completed requests map is empty. 59 | */ 60 | final def isEmpty: Boolean = 61 | map.isEmpty 62 | 63 | /** 64 | * Appends the specified result to the completed requests map. 65 | * 66 | * @deprecated 67 | * Usage of this method is deprecated as it leads to performance 68 | * degradation. Prefer using one of the constructors in the companion object 69 | * instead 70 | */ 71 | @deprecated("Use one of the constructors in the companion object instead. See scaladoc for more info", "0.7.0") 72 | final def insert[E, A](request: Request[E, A], result: Exit[E, A]): CompletedRequestMap = 73 | new CompletedRequestMap.Immutable(immutable.HashMap.from(map).updated(request, result)) 74 | 75 | /** 76 | * Appends the specified optional result to the completed request map. 77 | * 78 | * @deprecated 79 | * Usage of this method is deprecated as it leads to performance 80 | * degradation. Prefer using one of the constructors in the companion object 81 | * instead 82 | */ 83 | @deprecated("Use one of the constructors in the companion object instead. See scaladoc for more info", "0.7.0") 84 | final def insertOption[E, A](request: Request[E, A], result: Exit[E, Option[A]]): CompletedRequestMap = 85 | result match { 86 | case Exit.Failure(e) => insert(request, Exit.failCause(e)) 87 | case Exit.Success(Some(a)) => insert(request, Exit.succeed(a)) 88 | case Exit.Success(None) => self 89 | } 90 | 91 | /** 92 | * Retrieves the result of the specified request if it exists. 93 | */ 94 | final def lookup[E, A](request: Request[E, A]): Option[Exit[E, A]] = 95 | map.get(request).asInstanceOf[Option[Exit[E, A]]] 96 | 97 | /** 98 | * Collects all requests in a set. 99 | */ 100 | final def requests: Set[Request[_, _]] = 101 | map.keySet.asInstanceOf[Set[Request[_, _]]] 102 | 103 | final override def toString: String = 104 | s"CompletedRequestMap(${map.mkString(", ")})" 105 | 106 | final private[query] def underlying: collection.Map[Request[?, ?], Exit[Any, Any]] = 107 | map.asInstanceOf[collection.Map[Request[?, ?], Exit[Any, Any]]] 108 | } 109 | 110 | object CompletedRequestMap { 111 | 112 | def apply[E, A](entries: (Request[E, A], Exit[E, A])*): CompletedRequestMap = 113 | entries match { 114 | case Seq() => empty 115 | case Seq((req, resp)) => single(req, resp) 116 | case _ => fromIterable(entries) 117 | } 118 | 119 | val empty: CompletedRequestMap = 120 | new Immutable(immutable.HashMap.empty) 121 | 122 | /** 123 | * Combines all completed request maps into a single one. 124 | * 125 | * This method is left-associated, meaning that if a request is present in 126 | * multiple maps, it will be overriden by the last map in the list. 127 | */ 128 | def combine(maps: Iterable[CompletedRequestMap]): CompletedRequestMap = { 129 | val map = Mutable.empty(maps.foldLeft(0)(_ + _.map.size)) 130 | maps.foreach(map.addAll) 131 | map 132 | } 133 | 134 | /** 135 | * Constructs a completed requests map that "dies" all the specified requests 136 | * with the specified throwable 137 | */ 138 | def die[E, A](requests: Chunk[Request[E, A]], error: Throwable): CompletedRequestMap = { 139 | val map = Mutable.empty(requests.size) 140 | val exit = Exit.die(error) 141 | requests.foreach(map.update(_, exit)) 142 | map 143 | } 144 | 145 | /** 146 | * Constructs a completed requests map that fails all the specified requests 147 | * with the specified error 148 | */ 149 | def fail[E, A](requests: Chunk[Request[E, A]], error: E): CompletedRequestMap = { 150 | val map = Mutable.empty(requests.size) 151 | val exit = Exit.fail(error) 152 | requests.foreach(map.update(_, exit)) 153 | map 154 | } 155 | 156 | /** 157 | * Constructs a completed requests map that fails all the specified requests 158 | * with the specified cause 159 | */ 160 | def failCause[E, A](requests: Chunk[Request[E, A]], cause: Cause[E]): CompletedRequestMap = { 161 | val map = Mutable.empty(requests.size) 162 | val exit = Exit.failCause(cause) 163 | requests.foreach(map.update(_, exit)) 164 | map 165 | } 166 | 167 | /** 168 | * Constructs a completed requests map from the specified results. 169 | */ 170 | def fromIterable[E, A](iterable: Iterable[(Request[E, A], Exit[E, A])]): CompletedRequestMap = 171 | Mutable(mutable.HashMap.from(iterable)) 172 | 173 | /** 174 | * Constructs a completed requests map from the specified optional results. 175 | */ 176 | def fromIterableOption[E, A](iterable: Iterable[(Request[E, A], Exit[E, Option[A]])]): CompletedRequestMap = { 177 | val map = Mutable.empty(iterable.size) 178 | iterable.foreach { 179 | case (request, Exit.Failure(e)) => map.update(request, Exit.failCause(e)) 180 | case (request, Exit.Success(Some(a))) => map.update(request, Exit.succeed(a)) 181 | case (_, Exit.Success(None)) => () 182 | } 183 | map 184 | } 185 | 186 | /** 187 | * Constructs a completed requests map an iterable of A and functions that map 188 | * each A to a request and a result 189 | * 190 | * @param f1 191 | * function that maps each element of A to a request 192 | * @param f2 193 | * function that maps each element of A to an Exit, e.g., Exit.succeed(_) 194 | */ 195 | def fromIterableWith[E, A, B]( 196 | iterable: Iterable[A] 197 | )( 198 | f1: A => Request[E, B], 199 | f2: A => Exit[E, B] 200 | ): CompletedRequestMap = { 201 | val map = Mutable.empty(iterable.size) 202 | iterable.foreach(req => map.update(f1(req), f2(req))) 203 | map 204 | } 205 | 206 | /** 207 | * Constructs a completed requests map containing a single entry 208 | */ 209 | def single[E, A](request: Request[E, A], exit: Exit[E, A]): CompletedRequestMap = 210 | new Immutable(immutable.HashMap((request, exit))) 211 | 212 | /** 213 | * Unsafe API for constructing completed request maps. 214 | * 215 | * Constructing a [[CompletedRequestMap]] via these methods can improve 216 | * performance in some cases as they allow skipping the creation of 217 | * intermediate `Iterable[ Tuple2[_, _] ]`. 218 | * 219 | * NOTE: These methods are marked as unsafe because they do not check that the 220 | * requests and responses are of the same size. It is the responsibility of 221 | * the caller to ensure that the provided requests maps elements 1-to-1 to the 222 | * responses. 223 | */ 224 | val unsafe: UnsafeApi = new UnsafeApi {} 225 | 226 | trait UnsafeApi { 227 | 228 | def fromExits[E, A](requests: Chunk[Request[E, A]], responses: Chunk[Exit[E, A]]): CompletedRequestMap = 229 | fromWith(requests, responses)(identity, identity) 230 | 231 | def fromSuccesses[E, A](requests: Chunk[Request[E, A]], responses: Chunk[A]): CompletedRequestMap = 232 | fromWith(requests, responses)(identity, Exit.succeed) 233 | 234 | def fromWith[E, A1, A2, B](requests: Chunk[A1], responses: Chunk[A2])( 235 | f1: A1 => Request[E, B], 236 | f2: A2 => Exit[E, B] 237 | ): CompletedRequestMap = { 238 | val size = requests.size min responses.size 239 | val map = Mutable.empty(size) 240 | val reqs = requests.chunkIterator 241 | val resps = responses.chunkIterator 242 | var i = 0 243 | while (i < size) { 244 | map.update(f1(reqs.nextAt(i)), f2(resps.nextAt(i))) 245 | i += 1 246 | } 247 | map 248 | } 249 | 250 | } 251 | 252 | final private class Immutable(override protected val map: immutable.HashMap[Any, Exit[Any, Any]]) 253 | extends CompletedRequestMap 254 | 255 | final private[query] class Mutable private ( 256 | override protected val map: mutable.HashMap[Any, Exit[Any, Any]] 257 | ) extends CompletedRequestMap { self => 258 | import UtilsVersionSpecific._ 259 | 260 | def addAll(that: CompletedRequestMap): Unit = 261 | if (!that.isEmpty) self.map.addAll(that.map) 262 | 263 | def update[E, A](request: Request[E, A], exit: Exit[E, A]): Unit = 264 | map.update(request, exit) 265 | 266 | } 267 | 268 | private[query] object Mutable { 269 | 270 | def apply(map: mutable.HashMap[Any, Exit[Any, Any]]): CompletedRequestMap.Mutable = 271 | new Mutable(map) 272 | 273 | def empty(size: Int): CompletedRequestMap.Mutable = 274 | new Mutable(UtilsVersionSpecific.newHashMap(size)) 275 | 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /zio-query/shared/src/main/scala/zio/query/internal/BlockedRequests.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2023 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package zio.query.internal 18 | 19 | import zio.query._ 20 | import zio.query.internal.BlockedRequests._ 21 | import zio.stacktracer.TracingImplicits.disableAutoTrace 22 | import zio.{Chunk, Exit, Trace, UIO, Unsafe, ZEnvironment, ZIO} 23 | 24 | import scala.annotation.tailrec 25 | import scala.collection.compat._ 26 | import scala.collection.mutable 27 | import scala.collection.mutable.ListBuffer 28 | 29 | /** 30 | * `BlockedRequests` captures a collection of blocked requests as a data 31 | * structure. By doing this the library is able to preserve information about 32 | * which requests must be performed sequentially and which can be performed in 33 | * parallel, allowing for maximum possible batching and pipelining while 34 | * preserving ordering guarantees. 35 | */ 36 | private[query] sealed trait BlockedRequests[-R] { self => 37 | 38 | /** 39 | * Combines this collection of blocked requests with the specified collection 40 | * of blocked requests, in parallel. 41 | */ 42 | final def &&[R1 <: R](that: BlockedRequests[R1]): BlockedRequests[R1] = 43 | Both(self, that) 44 | 45 | /** 46 | * Combines this collection of blocked requests with the specified collection 47 | * of blocked requests, in sequence. 48 | */ 49 | final def ++[R1 <: R](that: BlockedRequests[R1]): BlockedRequests[R1] = 50 | Then(self, that) 51 | 52 | /** 53 | * Folds over the cases of this collection of blocked requests with the 54 | * specified functions. 55 | */ 56 | final def fold[Z](folder: Folder[R, Z]): Z = { 57 | sealed trait BlockedRequestsCase 58 | 59 | case object BothCase extends BlockedRequestsCase 60 | case object ThenCase extends BlockedRequestsCase 61 | 62 | @tailrec 63 | def loop(in: List[BlockedRequests[R]], out: List[Either[BlockedRequestsCase, Z]]): List[Z] = 64 | in match { 65 | case Single(dataSource, blockedRequest) :: blockedRequests => 66 | loop(blockedRequests, Right(folder.singleCase(dataSource, blockedRequest)) :: out) 67 | case Both(left, right) :: blockedRequests => 68 | loop(left :: right :: blockedRequests, Left(BothCase) :: out) 69 | case Then(left, right) :: blockedRequests => 70 | loop(left :: right :: blockedRequests, Left(ThenCase) :: out) 71 | case Empty :: blockedRequests => 72 | loop(blockedRequests, Right(folder.emptyCase) :: out) 73 | case Nil => 74 | out.foldLeft[List[Z]](List.empty) { 75 | case (acc, Right(blockedRequests)) => 76 | blockedRequests :: acc 77 | case (acc, Left(BothCase)) => 78 | val left :: right :: blockedRequests = (acc: @unchecked) 79 | folder.bothCase(left, right) :: blockedRequests 80 | case (acc, Left(ThenCase)) => 81 | val left :: right :: blockedRequests = (acc: @unchecked) 82 | folder.thenCase(left, right) :: blockedRequests 83 | } 84 | } 85 | 86 | loop(List(self), List.empty).head 87 | } 88 | 89 | final def isEmpty: Boolean = self eq Empty 90 | 91 | /** 92 | * Transforms all data sources with the specified data source aspect, which 93 | * can change the environment type of data sources but must preserve the 94 | * request type of each data source. 95 | */ 96 | final def mapDataSources[R1 <: R](f: DataSourceAspect[R1]): BlockedRequests[R1] = 97 | fold(Folder.MapDataSources(f)) 98 | 99 | /** 100 | * Provides each data source with part of its required environment. 101 | */ 102 | final def provideSomeEnvironment[R0](f: Described[ZEnvironment[R0] => ZEnvironment[R]]): BlockedRequests[R0] = 103 | fold(Folder.ProvideSomeEnvironment(f)) 104 | 105 | /** 106 | * Executes all requests, submitting requests to each data source in parallel. 107 | */ 108 | def run(implicit trace: Trace): ZIO[R, Nothing, Unit] = { 109 | val flattened = BlockedRequests.flatten(self) 110 | ZIO.foreachDiscard(flattened) { requestsByDataSource => 111 | ZIO.foreachParDiscard(requestsByDataSource.toIterable) { case (dataSource, sequential) => 112 | val requests = sequential.map(_.map(_.request)) 113 | 114 | dataSource 115 | .runAll(requests) 116 | .catchAllCause(cause => 117 | Exit.succeed { 118 | CompletedRequestMap.failCause( 119 | requests.flatten.asInstanceOf[Chunk[Request[Any, Any]]], 120 | cause 121 | ) 122 | } 123 | ) 124 | .flatMap { completedRequests => 125 | val completedRequestsM = mutable.HashMap.from(completedRequests.underlying) 126 | ZQuery.currentCache.getWith { 127 | case Some(cache) => 128 | completePromises(dataSource, sequential) { req => 129 | // Pop the entry, and fallback to the immutable one if we already removed it 130 | completedRequestsM.remove(req) orElse completedRequests.lookup(req) 131 | } 132 | // cache responses that were not requested but were completed by the DataSource 133 | if (completedRequestsM.nonEmpty) cacheLeftovers(cache, completedRequestsM) else Exit.unit 134 | case _ => { 135 | // No need to remove entries here since we don't need to know which ones we need to put in the cache 136 | ZIO.succeed(completePromises(dataSource, sequential)(completedRequestsM.get)) 137 | } 138 | } 139 | } 140 | } 141 | } 142 | } 143 | } 144 | 145 | private[query] object BlockedRequests { 146 | 147 | /** 148 | * The empty collection of blocked requests. 149 | */ 150 | val empty: BlockedRequests[Any] = 151 | Empty 152 | 153 | /** 154 | * Constructs a collection of blocked requests from the specified blocked 155 | * request and data source. 156 | */ 157 | def single[R, K](dataSource: DataSource[R, K], blockedRequest: BlockedRequest[K]): BlockedRequests[R] = 158 | Single(dataSource, blockedRequest) 159 | 160 | final case class Both[-R](left: BlockedRequests[R], right: BlockedRequests[R]) extends BlockedRequests[R] 161 | 162 | case object Empty extends BlockedRequests[Any] { 163 | override def run(implicit trace: Trace): ZIO[Any, Nothing, Unit] = Exit.unit 164 | } 165 | 166 | final case class Single[-R, A](dataSource: DataSource[R, A], blockedRequest: BlockedRequest[A]) 167 | extends BlockedRequests[R] 168 | 169 | final case class Then[-R](left: BlockedRequests[R], right: BlockedRequests[R]) extends BlockedRequests[R] 170 | 171 | trait Folder[+R, Z] { 172 | def emptyCase: Z 173 | def singleCase[A](dataSource: DataSource[R, A], blockedRequest: BlockedRequest[A]): Z 174 | def bothCase(left: Z, right: Z): Z 175 | def thenCase(left: Z, right: Z): Z 176 | } 177 | 178 | object Folder { 179 | 180 | final case class MapDataSources[R](f: DataSourceAspect[R]) extends Folder[R, BlockedRequests[R]] { 181 | def emptyCase: BlockedRequests[R] = 182 | Empty 183 | def singleCase[A](dataSource: DataSource[R, A], blockedRequest: BlockedRequest[A]): BlockedRequests[R] = 184 | Single(f(dataSource), blockedRequest) 185 | def bothCase(left: BlockedRequests[R], right: BlockedRequests[R]): BlockedRequests[R] = 186 | Both(left, right) 187 | def thenCase(left: BlockedRequests[R], right: BlockedRequests[R]): BlockedRequests[R] = 188 | Then(left, right) 189 | } 190 | 191 | final case class ProvideSomeEnvironment[R0, R](f: Described[ZEnvironment[R0] => ZEnvironment[R]]) 192 | extends Folder[R, BlockedRequests[R0]] { 193 | def emptyCase: BlockedRequests[R0] = 194 | Empty 195 | def singleCase[A](dataSource: DataSource[R, A], blockedRequest: BlockedRequest[A]): BlockedRequests[R0] = 196 | Single(dataSource.provideSomeEnvironment(f), blockedRequest) 197 | def bothCase(left: BlockedRequests[R0], right: BlockedRequests[R0]): BlockedRequests[R0] = 198 | Both(left, right) 199 | def thenCase(left: BlockedRequests[R0], right: BlockedRequests[R0]): BlockedRequests[R0] = 200 | Then(left, right) 201 | } 202 | } 203 | 204 | /** 205 | * Flattens a collection of blocked requests into a collection of pipelined 206 | * and batched requests that can be submitted for execution. 207 | */ 208 | private def flatten[R]( 209 | blockedRequests: BlockedRequests[R] 210 | ): List[Sequential[R]] = { 211 | 212 | @tailrec 213 | def loop( 214 | blockedRequests: List[BlockedRequests[R]], 215 | flattened: List[Sequential[R]] 216 | ): List[Sequential[R]] = { 217 | val parallel = Parallel.empty 218 | val sequential = ListBuffer.empty[BlockedRequests[R]] 219 | blockedRequests.foreach(step(parallel, sequential)) 220 | val updated = merge(flattened, parallel) 221 | if (sequential.isEmpty) updated.reverse 222 | else loop(sequential.result(), updated) 223 | } 224 | 225 | loop(List(blockedRequests), List.empty) 226 | } 227 | 228 | /** 229 | * Takes one step in evaluating a collection of blocked requests, returning a 230 | * collection of blocked requests that can be performed in parallel and a list 231 | * of blocked requests that must be performed sequentially after those 232 | * requests. 233 | */ 234 | private def step[R]( 235 | parallel: Parallel[R], 236 | sequential: ListBuffer[BlockedRequests[R]] 237 | )(c: BlockedRequests[R]): Unit = { 238 | 239 | @tailrec 240 | def loop( 241 | blockedRequests: BlockedRequests[R], 242 | stack: List[BlockedRequests[R]] 243 | ): Unit = 244 | blockedRequests match { 245 | case Single(dataSource, request) => 246 | parallel.addOne(dataSource, request) 247 | if (stack ne Nil) loop(stack.head, stack.tail) 248 | case Both(left, right) => 249 | loop(left, right :: stack) 250 | case Then(left, right) => 251 | left match { 252 | case Empty => loop(right, stack) 253 | case Then(l, r) => loop(Then(l, Then(r, right)), stack) 254 | case o => 255 | if (right ne Empty) sequential.prepend(right) 256 | loop(o, stack) 257 | } 258 | case Empty => 259 | if (stack ne Nil) loop(stack.head, stack.tail) 260 | } 261 | 262 | loop(c, List.empty) 263 | } 264 | 265 | /** 266 | * Merges a collection of requests that must be executed sequentially with a 267 | * collection of requests that can be executed in parallel. If the collections 268 | * are both from the same single data source then the requests can be 269 | * pipelined while preserving ordering guarantees. 270 | */ 271 | private def merge[R](sequential: List[Sequential[R]], parallel: Parallel[R]): List[Sequential[R]] = 272 | if (sequential.isEmpty) 273 | parallel.sequential :: Nil 274 | else if (parallel.isEmpty) 275 | sequential 276 | else if ({ 277 | val seqHead = sequential.head 278 | seqHead.size == 1 && parallel.size == 1 && seqHead.head == parallel.head 279 | }) 280 | (sequential.head ++ parallel.sequential) :: sequential.tail 281 | else 282 | parallel.sequential :: sequential 283 | 284 | private def completePromises( 285 | dataSource: DataSource[_, Any], 286 | sequential: Chunk[Chunk[BlockedRequest[Any]]] 287 | )(get: Request[?, ?] => Option[Exit[Any, Any]]): Unit = 288 | sequential.foreach { 289 | _.foreach { br => 290 | val req = br.request 291 | val res = get(req) match { 292 | case Some(exit) => exit.asInstanceOf[Exit[br.Failure, br.Success]] 293 | case None => Exit.die(QueryFailure(dataSource, req)) 294 | } 295 | br.result.unsafe.done(res)(Unsafe.unsafe) 296 | } 297 | } 298 | 299 | private def cacheLeftovers( 300 | cache: Cache, 301 | map: mutable.HashMap[Request[_, _], Exit[Any, Any]] 302 | )(implicit trace: Trace): UIO[Unit] = 303 | cache match { 304 | case cache: Cache.Default => 305 | ZIO.succeedUnsafe { implicit unsafe => 306 | map.foreach { case (request: Request[Any, Any], exit) => 307 | cache 308 | .lookupUnsafe(request) 309 | .merge 310 | .unsafe 311 | .done(exit) 312 | } 313 | } 314 | case cache => 315 | ZIO.foreachDiscard(map) { case (request: Request[Any, Any], exit) => 316 | cache.lookup(request).flatMap(_.merge.done(exit)) 317 | } 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /zio-query/shared/src/main/scala/zio/query/DataSource.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2023 John A. De Goes and the ZIO Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package zio.query 18 | 19 | import zio.stacktracer.TracingImplicits.disableAutoTrace 20 | import zio.{Chunk, Exit, Trace, ZEnvironment, ZIO} 21 | 22 | /** 23 | * A `DataSource[R, A]` requires an environment `R` and is capable of executing 24 | * requests of type `A`. 25 | * 26 | * Data sources must implement the method `runAll` which takes a collection of 27 | * requests and returns an effect with a `CompletedRequestMap` containing a 28 | * mapping from requests to results. The type of the collection of requests is a 29 | * `Chunk[Chunk[A]]`. The outer `Chunk` represents batches of requests that must 30 | * be performed sequentially. The inner `Chunk` represents a batch of requests 31 | * that can be performed in parallel. This allows data sources to introspect on 32 | * all the requests being executed and optimize the query. 33 | * 34 | * Data sources will typically be parameterized on a subtype of `Request[A]`, 35 | * though that is not strictly necessarily as long as the data source can map 36 | * the request type to a `Request[A]`. Data sources can then pattern match on 37 | * the collection of requests to determine the information requested, execute 38 | * the query, and place the results into the `CompletedRequestsMap` one of the 39 | * constructors in [[CompletedRequestMap$]]. Data sources must provide results 40 | * for all requests received. Failure to do so will cause a query to die with a 41 | * `QueryFailure` when run. 42 | */ 43 | trait DataSource[-R, -A] { self => 44 | 45 | /** 46 | * Syntax for adding aspects. 47 | */ 48 | final def @@[R1 <: R](aspect: DataSourceAspect[R1]): DataSource[R1, A] = 49 | aspect(self) 50 | 51 | /** 52 | * The data source's identifier. 53 | */ 54 | val identifier: String 55 | 56 | /** 57 | * Execute a collection of requests. The outer `Chunk` represents batches of 58 | * requests that must be performed sequentially. The inner `Chunk` represents 59 | * a batch of requests that can be performed in parallel. 60 | */ 61 | def runAll(requests: Chunk[Chunk[A]])(implicit trace: Trace): ZIO[R, Nothing, CompletedRequestMap] 62 | 63 | /** 64 | * Returns a data source that executes at most `n` requests in parallel. 65 | */ 66 | def batchN(n: Int): DataSource[R, A] = 67 | new DataSource[R, A] { 68 | val identifier = s"${self}.batchN($n)" 69 | def runAll(requests: Chunk[Chunk[A]])(implicit trace: Trace): ZIO[R, Nothing, CompletedRequestMap] = 70 | if (n < 1) 71 | ZIO.die(new IllegalArgumentException("batchN: n must be at least 1")) 72 | else 73 | self.runAll(requests.foldLeft(Chunk.newBuilder[Chunk[A]])(_ addAll _.grouped(n)).result()) 74 | } 75 | 76 | /** 77 | * Returns a new data source that executes requests of type `B` using the 78 | * specified function to transform `B` requests into requests that this data 79 | * source can execute. 80 | */ 81 | final def contramap[B](f: Described[B => A]): DataSource[R, B] = 82 | new DataSource[R, B] { 83 | val identifier = s"${self.identifier}.contramap(${f.description})" 84 | def runAll(requests: Chunk[Chunk[B]])(implicit trace: Trace): ZIO[R, Nothing, CompletedRequestMap] = 85 | self.runAll(requests.map(_.map(f.value))) 86 | } 87 | 88 | /** 89 | * Returns a new data source that executes requests of type `B` using the 90 | * specified effectual function to transform `B` requests into requests that 91 | * this data source can execute. 92 | */ 93 | final def contramapZIO[R1 <: R, B](f: Described[B => ZIO[R1, Nothing, A]]): DataSource[R1, B] = 94 | new DataSource[R1, B] { 95 | import scala.collection.compat.BuildFrom._ // Required for Scala 3 96 | 97 | val identifier = s"${self.identifier}.contramapZIO(${f.description})" 98 | def runAll(requests: Chunk[Chunk[B]])(implicit trace: Trace): ZIO[R1, Nothing, CompletedRequestMap] = 99 | ZIO.foreach(requests)(ZIO.foreachPar(_)(f.value)).flatMap(self.runAll) 100 | } 101 | 102 | /** 103 | * Returns a new data source that executes requests of type `C` using the 104 | * specified function to transform `C` requests into requests that either this 105 | * data source or that data source can execute. 106 | */ 107 | final def eitherWith[R1 <: R, B, C]( 108 | that: DataSource[R1, B] 109 | )(f: Described[C => Either[A, B]]): DataSource[R1, C] = 110 | new DataSource[R1, C] { 111 | val identifier = s"${self.identifier}.eitherWith(${that.identifier})(${f.description})" 112 | def runAll(requests: Chunk[Chunk[C]])(implicit trace: Trace): ZIO[R1, Nothing, CompletedRequestMap] = 113 | ZIO.suspendSucceed { 114 | val iter = requests.iterator 115 | val crm = CompletedRequestMap.Mutable.empty(requests.foldLeft(0)(_ + _.size)) 116 | ZIO 117 | .whileLoop(iter.hasNext) { 118 | val reqs = iter.next() 119 | val (as, bs) = reqs.partitionMap(f.value) 120 | self.runAll(Chunk.single(as)) <&> that.runAll(Chunk.single(bs)) 121 | } { case (l, r) => 122 | crm.addAll(l) 123 | crm.addAll(r) 124 | } 125 | .as(crm) 126 | } 127 | } 128 | 129 | override final def equals(that: Any): Boolean = 130 | that match { 131 | case that: DataSource[_, _] => this.identifier == that.identifier 132 | case _ => false 133 | } 134 | 135 | override final def hashCode: Int = 136 | identifier.hashCode 137 | 138 | /** 139 | * Provides this data source with its required environment. 140 | */ 141 | final def provideEnvironment(r: Described[ZEnvironment[R]]): DataSource[Any, A] = 142 | provideSomeEnvironment(Described(_ => r.value, s"_ => ${r.description}")) 143 | 144 | /** 145 | * Provides this data source with part of its required environment. 146 | */ 147 | final def provideSomeEnvironment[R0]( 148 | f: Described[ZEnvironment[R0] => ZEnvironment[R]] 149 | ): DataSource[R0, A] = 150 | new DataSource[R0, A] { 151 | val identifier = s"${self.identifier}.provideSomeEnvironment(${f.description})" 152 | def runAll(requests: Chunk[Chunk[A]])(implicit trace: Trace): ZIO[R0, Nothing, CompletedRequestMap] = 153 | self.runAll(requests).provideSomeEnvironment(f.value) 154 | } 155 | 156 | /** 157 | * Submits a request to this datasource and returns the result. 158 | */ 159 | final def query[E, A1 <: A, B](request: A1)(implicit ev: A1 <:< Request[E, B], trace: Trace): ZQuery[R, E, B] = 160 | ZQuery.fromRequest(request)(self) 161 | 162 | /** 163 | * Submits a Chunk of requests to this datasource and returns the results in 164 | * the same order 165 | */ 166 | final def queryAll[E, A1 <: A, B]( 167 | requests: Chunk[A1] 168 | )(implicit ev: A1 <:< Request[E, B], trace: Trace): ZQuery[R, E, Chunk[B]] = 169 | ZQuery.fromRequests(requests)(self) 170 | 171 | /** 172 | * Returns a new data source that executes requests by sending them to this 173 | * data source and that data source, returning the results from the first data 174 | * source to complete and safely interrupting the loser. 175 | */ 176 | final def race[R1 <: R, A1 <: A](that: DataSource[R1, A1]): DataSource[R1, A1] = 177 | new DataSource[R1, A1] { 178 | val identifier = s"${self.identifier}.race(${that.identifier})" 179 | def runAll(requests: Chunk[Chunk[A1]])(implicit trace: Trace): ZIO[R1, Nothing, CompletedRequestMap] = 180 | self.runAll(requests).race(that.runAll(requests)) 181 | } 182 | 183 | override final def toString: String = 184 | identifier 185 | } 186 | 187 | object DataSource { 188 | import UtilsVersionSpecific._ 189 | 190 | /** 191 | * A data source that executes requests that can be performed in parallel in 192 | * batches but does not further optimize batches of requests that must be 193 | * performed sequentially. 194 | */ 195 | trait Batched[-R, -A] extends DataSource[R, A] { 196 | def run(requests: Chunk[A])(implicit trace: Trace): ZIO[R, Nothing, CompletedRequestMap] 197 | 198 | final def runAll(requests: Chunk[Chunk[A]])(implicit trace: Trace): ZIO[R, Nothing, CompletedRequestMap] = 199 | requests.size match { 200 | case 0 => ZIO.succeed(CompletedRequestMap.empty) 201 | case 1 => 202 | val reqs0 = requests.head 203 | if (reqs0.nonEmpty) run(reqs0) else ZIO.succeed(CompletedRequestMap.empty) 204 | case _ => 205 | ZIO.suspendSucceed { 206 | val crm = CompletedRequestMap.Mutable.empty(requests.foldLeft(0)(_ + _.size)) 207 | val iter = requests.iterator 208 | ZIO 209 | .whileLoop(iter.hasNext) { 210 | val reqs = iter.next() 211 | val newRequests = if (crm.isEmpty) reqs else reqs.filterNot(crm.contains) 212 | ZIO.when(newRequests.nonEmpty)(run(newRequests)) 213 | } { 214 | case Some(map) => crm.addAll(map) 215 | case _ => () 216 | } 217 | .as(crm) 218 | } 219 | } 220 | 221 | } 222 | 223 | object Batched { 224 | 225 | /** 226 | * Constructs a data source from a function taking a collection of requests 227 | * and returning a `CompletedRequestMap`. 228 | */ 229 | def make[R, A](name: String)(f: Chunk[A] => ZIO[R, Nothing, CompletedRequestMap]): DataSource[R, A] = 230 | new DataSource.Batched[R, A] { 231 | val identifier: String = name 232 | def run(requests: Chunk[A])(implicit trace: Trace): ZIO[R, Nothing, CompletedRequestMap] = 233 | f(requests) 234 | } 235 | } 236 | 237 | /** 238 | * Constructs a data source from a pure function. 239 | */ 240 | def fromFunction[A, B]( 241 | name: String 242 | )(f: A => B)(implicit ev: A <:< Request[Nothing, B]): DataSource[Any, A] = 243 | new DataSource.Batched[Any, A] { 244 | val identifier: String = name 245 | def run(requests: Chunk[A])(implicit trace: Trace): ZIO[Any, Nothing, CompletedRequestMap] = 246 | ZIO.succeed(CompletedRequestMap.fromIterableWith(requests)(ev.apply, a => Exit.succeed(f(a)))) 247 | } 248 | 249 | /** 250 | * Constructs a data source from a pure function that takes a list of requests 251 | * and returns a list of results of the same size. Each item in the result 252 | * list must correspond to the item at the same index in the request list. 253 | */ 254 | def fromFunctionBatched[A, B]( 255 | name: String 256 | )(f: Chunk[A] => Chunk[B])(implicit ev: A <:< Request[Nothing, B]): DataSource[Any, A] = 257 | fromFunctionBatchedZIO(name)(as => Exit.succeed(f(as))) 258 | 259 | /** 260 | * Constructs a data source from a pure function that takes a list of requests 261 | * and returns a list of optional results of the same size. Each item in the 262 | * result list must correspond to the item at the same index in the request 263 | * list. 264 | */ 265 | def fromFunctionBatchedOption[A, B]( 266 | name: String 267 | )(f: Chunk[A] => Chunk[Option[B]])(implicit ev: A <:< Request[Nothing, B]): DataSource[Any, A] = 268 | fromFunctionBatchedOptionZIO(name)(as => Exit.succeed(f(as))) 269 | 270 | /** 271 | * Constructs a data source from an effectual function that takes a list of 272 | * requests and returns a list of optional results of the same size. Each item 273 | * in the result list must correspond to the item at the same index in the 274 | * request list. 275 | */ 276 | def fromFunctionBatchedOptionZIO[R, E, A, B]( 277 | name: String 278 | )(f: Chunk[A] => ZIO[R, E, Chunk[Option[B]]])(implicit ev: A <:< Request[E, B]): DataSource[R, A] = 279 | new DataSource.Batched[R, A] { 280 | val identifier: String = name 281 | def run(requests: Chunk[A])(implicit trace: Trace): ZIO[R, Nothing, CompletedRequestMap] = 282 | f(requests) 283 | .foldCause( 284 | e => requests.map(a => (ev(a), Exit.failCause(e))), 285 | bs => requests.zipWith(bs)((a, b) => (ev(a), Exit.succeed(b))) 286 | ) 287 | .map(CompletedRequestMap.fromIterableOption) 288 | } 289 | 290 | /** 291 | * Constructs a data source from a function that takes a list of requests and 292 | * returns a list of results of the same size. Uses the specified function to 293 | * associate each result with the corresponding effect, allowing the function 294 | * to return the list of results in a different order than the list of 295 | * requests. 296 | */ 297 | def fromFunctionBatchedWith[A, B]( 298 | name: String 299 | )(f: Chunk[A] => Chunk[B], g: B => Request[Nothing, B])(implicit 300 | ev: A <:< Request[Nothing, B] 301 | ): DataSource[Any, A] = 302 | fromFunctionBatchedWithZIO[Any, Nothing, A, B](name)(as => Exit.succeed(f(as)), g) 303 | 304 | /** 305 | * Constructs a data source from an effectual function that takes a list of 306 | * requests and returns a list of results of the same size. Uses the specified 307 | * function to associate each result with the corresponding effect, allowing 308 | * the function to return the list of results in a different order than the 309 | * list of requests. 310 | */ 311 | def fromFunctionBatchedWithZIO[R, E, A, B]( 312 | name: String 313 | )(f: Chunk[A] => ZIO[R, E, Chunk[B]], g: B => Request[E, B])(implicit 314 | ev: A <:< Request[E, B] 315 | ): DataSource[R, A] = 316 | new DataSource.Batched[R, A] { 317 | val identifier: String = name 318 | def run(requests: Chunk[A])(implicit trace: Trace): ZIO[R, Nothing, CompletedRequestMap] = 319 | f(requests) 320 | .foldCause( 321 | e => CompletedRequestMap.failCause(ev.liftCo(requests), e), 322 | bs => CompletedRequestMap.unsafe.fromWith(bs, bs)(g(_), Exit.succeed) 323 | ) 324 | 325 | } 326 | 327 | /** 328 | * Constructs a data source from an effectual function that takes a list of 329 | * requests and returns a list of results of the same size. Each item in the 330 | * result list must correspond to the item at the same index in the request 331 | * list. 332 | */ 333 | def fromFunctionBatchedZIO[R, E, A, B]( 334 | name: String 335 | )(f: Chunk[A] => ZIO[R, E, Chunk[B]])(implicit ev: A <:< Request[E, B]): DataSource[R, A] = 336 | new DataSource.Batched[R, A] { 337 | val identifier: String = name 338 | def run(requests: Chunk[A])(implicit trace: Trace): ZIO[R, Nothing, CompletedRequestMap] = 339 | f(requests) 340 | .foldCause( 341 | e => CompletedRequestMap.failCause(ev.liftCo(requests), e), 342 | CompletedRequestMap.unsafe.fromSuccesses(ev.liftCo(requests), _) 343 | ) 344 | } 345 | 346 | /** 347 | * Constructs a data source from an effectual function. 348 | */ 349 | def fromFunctionZIO[R, E, A, B]( 350 | name: String 351 | )(f: A => ZIO[R, E, B])(implicit ev: A <:< Request[E, B]): DataSource[R, A] = 352 | new DataSource.Batched[R, A] { 353 | val identifier: String = name 354 | def run(requests: Chunk[A])(implicit trace: Trace): ZIO[R, Nothing, CompletedRequestMap] = 355 | ZIO 356 | .foreachPar(requests)(a => f(a).exit) 357 | .map(CompletedRequestMap.unsafe.fromExits(ev.liftCo(requests), _)) 358 | } 359 | 360 | /** 361 | * Constructs a data source from a pure function that may not provide results 362 | * for all requests received. 363 | */ 364 | def fromFunctionOption[A, B]( 365 | name: String 366 | )(f: A => Option[B])(implicit ev: A <:< Request[Nothing, B]): DataSource[Any, A] = 367 | fromFunctionOptionZIO(name)(a => Exit.succeed(f(a))) 368 | 369 | /** 370 | * Constructs a data source from an effectual function that may not provide 371 | * results for all requests received. 372 | */ 373 | def fromFunctionOptionZIO[R, E, A, B]( 374 | name: String 375 | )(f: A => ZIO[R, E, Option[B]])(implicit ev: A <:< Request[E, B]): DataSource[R, A] = 376 | new DataSource.Batched[R, A] { 377 | val identifier: String = name 378 | def run(requests: Chunk[A])(implicit trace: Trace): ZIO[R, Nothing, CompletedRequestMap] = 379 | ZIO 380 | .foreachPar(requests)(a => f(a).exit.map((ev(a), _))) 381 | .map(CompletedRequestMap.fromIterableOption) 382 | } 383 | 384 | /** 385 | * Constructs a data source from a function taking a collection of requests 386 | * and returning a `CompletedRequestMap`. 387 | */ 388 | def make[R, A](name: String)(f: Chunk[Chunk[A]] => ZIO[R, Nothing, CompletedRequestMap]): DataSource[R, A] = 389 | new DataSource[R, A] { 390 | val identifier: String = name 391 | def runAll(requests: Chunk[Chunk[A]])(implicit trace: Trace): ZIO[R, Nothing, CompletedRequestMap] = 392 | f(requests) 393 | } 394 | 395 | /** 396 | * A data source that never executes requests. 397 | */ 398 | val never: DataSource[Any, Any] = 399 | new DataSource[Any, Any] { 400 | val identifier = "never" 401 | def runAll(requests: Chunk[Chunk[Any]])(implicit trace: Trace): ZIO[Any, Nothing, CompletedRequestMap] = 402 | ZIO.never 403 | } 404 | 405 | } 406 | -------------------------------------------------------------------------------- /zio-query/shared/src/test/scala/zio/query/ZQuerySpec.scala: -------------------------------------------------------------------------------- 1 | package zio.query 2 | 3 | import zio._ 4 | import zio.query.QueryAspect._ 5 | import zio.query.internal.QueryScope 6 | import zio.test.Assertion._ 7 | import zio.test.TestAspect.{after, nonFlaky, silent} 8 | import zio.test.{TestClock, TestConsole, TestEnvironment, _} 9 | 10 | object ZQuerySpec extends ZIOBaseSpec { 11 | 12 | override def spec: Spec[TestEnvironment, Any] = 13 | suite("ZQuerySpec")( 14 | test("N + 1 selects problem") { 15 | for { 16 | _ <- getAllUserNames.run 17 | log <- TestConsole.output 18 | } yield assert(log)(hasSize(equalTo(2))) 19 | }, 20 | test("mapError does not prevent batching") { 21 | implicit val canFail = zio.CanFail 22 | val a = getUserNameById(1).zip(getUserNameById(2)).mapError(identity) 23 | val b = getUserNameById(3).zip(getUserNameById(4)).mapError(identity) 24 | for { 25 | _ <- ZQuery.collectAllPar(List(a, b)).run 26 | log <- TestConsole.output 27 | } yield assert(log)(hasSize(equalTo(2))) 28 | }, 29 | test("failure to complete request is query failure") { 30 | for { 31 | result <- getUserNameById(27).run.exit 32 | } yield assert(result)(dies(equalTo(QueryFailure(UserRequestDataSource, GetNameById(27))))) 33 | }, 34 | test("query failure is correctly reported") { 35 | val failure = QueryFailure(UserRequestDataSource, GetNameById(27)) 36 | assert(failure.getMessage)( 37 | equalTo("Data source UserRequestDataSource did not complete request GetNameById(27).") 38 | ) 39 | }, 40 | test("timed does not prevent batching") { 41 | val a = getUserNameById(1).zip(getUserNameById(2)).timed 42 | val b = getUserNameById(3).zip(getUserNameById(4)) 43 | for { 44 | _ <- ZQuery.collectAllPar(List(a, b)).run 45 | log <- TestConsole.output 46 | } yield assert(log)(hasSize(equalTo(2))) 47 | }, 48 | test("optional converts a query to one that returns its value optionally") { 49 | for { 50 | result <- getUserNameById(27).map(identity).optional.run 51 | } yield assert(result)(isNone) 52 | }, 53 | suite("zip")( 54 | test("arbitrary effects are executed in order") { 55 | for { 56 | ref <- Ref.make(List.empty[Int]) 57 | query1 = ZQuery.fromZIO(ref.update(1 :: _)) 58 | query2 = ZQuery.fromZIO(ref.update(2 :: _)) 59 | _ <- (query1 *> query2).run 60 | result <- ref.get 61 | } yield assert(result)(equalTo(List(2, 1))) 62 | } @@ nonFlaky, 63 | test("requests are executed in order") { 64 | val query = Cache.put(0, 1) *> Cache.getAll <* Cache.put(1, -1) 65 | assertZIO(query.run)(equalTo(Map(0 -> 1))) 66 | } @@ after(Cache.clear) @@ nonFlaky, 67 | test("requests are executed in order after parallel execution") { 68 | val query = 69 | (Cache.putIfAbsent(0, 0) &> Cache.putIfAbsent(1, 1)) *> 70 | Cache.getAll <* 71 | (Cache.putIfAbsent(2, -1) &> Cache.putIfAbsent(3, -1)) 72 | assertZIO(query.run)(equalTo(Map(0 -> 0, 1 -> 1))) 73 | } @@ after(Cache.clear) @@ nonFlaky, 74 | test("requests are pipelined") { 75 | val query = Cache.put(0, 1) *> Cache.getAll <* Cache.put(1, -1) 76 | assertZIO(query.run *> Cache.log)(hasSize(equalTo(1))) 77 | } @@ after(Cache.clear) @@ nonFlaky, 78 | test("intervening flatMap prevents pipelining") { 79 | val query = Cache.put(0, 1).flatMap(ZQuery.succeed(_)) *> Cache.getAll <* Cache.put(1, -1) 80 | assertZIO(query.run *> Cache.log)(hasSize(equalTo(2))) 81 | } @@ after(Cache.clear) @@ nonFlaky, 82 | test("trailing flatMap does not prevent pipelining") { 83 | val query = Cache.put(0, 1) *> Cache.getAll <* Cache.put(1, -1).flatMap(ZQuery.succeed(_)) 84 | assertZIO(query.run *> Cache.log)(hasSize(equalTo(1))) 85 | } @@ after(Cache.clear) @@ nonFlaky, 86 | test("short circuits on failure") { 87 | for { 88 | ref <- Ref.make(true) 89 | query = ZQuery.fail("fail") *> ZQuery.fromZIO(ref.set(false)) 90 | _ <- query.run.ignore 91 | result <- ref.get 92 | } yield assert(result)(isTrue) 93 | } @@ nonFlaky, 94 | test("does not deduplicate uncached requests") { 95 | val query = Cache.getAll *> Cache.put(0, 1) *> Cache.getAll 96 | assertZIO(query.uncached.run)(equalTo(Map(0 -> 1))) 97 | } @@ nonFlaky 98 | ).provide(Cache.live), 99 | suite("zipBatched")( 100 | test("queries to multiple data sources can be executed in parallel") { 101 | for { 102 | promise <- Promise.make[Nothing, Unit] 103 | _ <- (neverQuery <~> succeedQuery(promise)).run.fork 104 | _ <- promise.await 105 | } yield assertCompletes 106 | }, 107 | test("arbitrary effects are executed in order") { 108 | for { 109 | ref <- Ref.make(List.empty[Int]) 110 | query1 = ZQuery.fromZIO(ref.update(1 :: _)) 111 | query2 = ZQuery.fromZIO(ref.update(2 :: _)) 112 | _ <- (query1 ~> query2).run 113 | result <- ref.get 114 | } yield assert(result)(equalTo(List(2, 1))) 115 | } @@ nonFlaky 116 | ), 117 | suite("zipPar")( 118 | test("queries to multiple data sources can be executed in parallel") { 119 | for { 120 | promise <- Promise.make[Nothing, Unit] 121 | _ <- (neverQuery <&> succeedQuery(promise)).run.fork 122 | _ <- promise.await 123 | } yield assertCompletes 124 | }, 125 | test("arbitrary effects can be executed in parallel") { 126 | for { 127 | promise <- Promise.make[Nothing, Unit] 128 | _ <- (ZQuery.never <&> ZQuery.fromZIO(promise.succeed(()))).run.fork 129 | _ <- promise.await 130 | } yield assertCompletes 131 | }, 132 | test("does not prevent batching") { 133 | for { 134 | _ <- ZQuery.collectAllPar(List.fill(100)(getAllUserNames)).run 135 | log <- TestConsole.output 136 | } yield assert(log)(hasSize(equalTo(2))) 137 | } @@ nonFlaky 138 | ), 139 | test("stack safety") { 140 | val effect = (0 to 100000) 141 | .map(ZQuery.succeed(_)) 142 | .foldLeft(ZQuery.succeed(0)) { (query1, query2) => 143 | for { 144 | acc <- query1 145 | i <- query2 146 | } yield acc + i 147 | } 148 | .run 149 | assertZIO(effect)(equalTo(705082704)) 150 | }, 151 | test("data sources can be raced") { 152 | for { 153 | promise <- Promise.make[Nothing, Unit] 154 | _ <- raceQuery(promise).run 155 | _ <- promise.await 156 | } yield assertCompletes 157 | }, 158 | test("max batch size") { 159 | val query = getAllUserNames @@ maxBatchSize(3) 160 | for { 161 | result <- query.run 162 | log <- TestConsole.output 163 | } yield assert(result)(hasSameElements(userNames.values)) && 164 | assert(log)(hasSize(equalTo(10))) 165 | }, 166 | test("multiple data sources do not prevent batching") { 167 | for { 168 | _ <- ZQuery.collectAllPar(List(getFoo, getBar)).run 169 | log <- TestConsole.output 170 | } yield assert(log)(hasSize(equalTo(2))) 171 | }, 172 | test("efficiency of large queries") { 173 | val query = for { 174 | users <- ZQuery.fromZIO( 175 | ZIO.succeed( 176 | List.tabulate(Sources.totalCount)(id => User(id, "user name", id, id)) 177 | ) 178 | ) 179 | richUsers <- ZQuery.foreachPar(users) { user => 180 | Sources 181 | .getPayment(user.paymentId) 182 | .zip(Sources.getAddress(user.addressId)) 183 | .map { case (payment, address) => 184 | (user, payment, address) 185 | } 186 | } 187 | } yield richUsers.size 188 | assertZIO(query.run)(equalTo(Sources.totalCount)) 189 | }, 190 | test("data sources can return additional results") { 191 | val getSome = ZQuery.foreachPar(List(3, 4))(get).map(_.toSet) 192 | val query = getAll *> getSome 193 | for { 194 | result <- query.run 195 | output <- TestConsole.output 196 | } yield assert(result)(equalTo(Set("c", "d"))) && 197 | assert(output)(equalTo(Vector("getAll called\n"))) 198 | }, 199 | suite("caching results fulfilled by datasources")( 200 | test("caching enabled") { 201 | for { 202 | cache <- zio.query.Cache.empty 203 | query = for { 204 | res <- ZQuery.fromRequest(Req.Get(1))(dsCompletingMoreRequests) 205 | } yield res 206 | requestResult <- query.runCache(cache) 207 | oneToTen = (1 to 10).toList 208 | cachedResults <- ZIO.foreach(oneToTen)(i => cache.get(Req.Get(i)).flatMap(_.await)) 209 | cacheCheck = cachedResults == oneToTen.map(_.toString) 210 | } yield assertTrue(requestResult == "1", cacheCheck) 211 | }, 212 | test("caching disabled") { 213 | for { 214 | cache <- zio.query.Cache.empty 215 | query = for { 216 | res <- ZQuery.fromRequestUncached(Req.Get(1))(dsCompletingMoreRequests) 217 | } yield res 218 | requestResult <- query.runCache(cache) 219 | oneToTen = (1 to 10).toList 220 | cachedResults <- ZIO.foreach(oneToTen)(i => cache.get(Req.Get(i)).isFailure) 221 | cacheCheck = cachedResults.forall(identity) 222 | } yield assertTrue(requestResult == "1", cacheCheck) 223 | } 224 | ), 225 | test("requests can be removed from the cache") { 226 | for { 227 | cache <- zio.query.Cache.empty 228 | query = for { 229 | _ <- getUserNameById(1) 230 | _ <- ZQuery.fromZIO(cache.remove(GetNameById(1))) 231 | _ <- getUserNameById(1) 232 | } yield () 233 | _ <- query.runCache(cache) 234 | log <- TestConsole.output 235 | } yield assert(log)(hasSize(equalTo(2))) 236 | }, 237 | suite("timeout")( 238 | test("times out a query that does not complete") { 239 | for { 240 | fiber <- ZQuery.never.timeout(1.second).run.fork 241 | _ <- TestClock.adjust(1.second) 242 | _ <- fiber.join 243 | } yield assertCompletes 244 | }, 245 | test("prevents subsequent requests to data sources from being executed") { 246 | for { 247 | fiber <- (ZQuery.fromZIO(ZIO.sleep(2.seconds)) *> neverQuery).timeout(1.second).run.fork 248 | _ <- TestClock.adjust(2.second) 249 | _ <- fiber.join 250 | } yield assertCompletes 251 | } 252 | ), 253 | test("regional caching should work with parallelism") { 254 | val left = for { 255 | _ <- getUserNameById(1) 256 | _ <- ZQuery.fromZIO(ZIO.sleep(1000.milliseconds)) 257 | _ <- getUserNameById(1) 258 | } yield () 259 | val right = for { 260 | _ <- getUserNameById(2) 261 | _ <- ZQuery.fromZIO(ZIO.sleep(500.milliseconds)) 262 | } yield () 263 | val query = left.uncached.zipPar(right.cached) 264 | for { 265 | fiber <- query.run.fork 266 | _ <- TestClock.adjust(500.milliseconds) 267 | _ <- TestClock.adjust(1000.milliseconds) 268 | _ <- fiber.join 269 | log <- TestConsole.output 270 | } yield assert(log)(hasSize(equalTo(2))) && 271 | assert(log)(hasAt(0)(containsString("GetNameById(1)"))) && 272 | assert(log)(hasAt(0)(containsString("GetNameById(2)"))) && 273 | assert(log)(hasAt(1)(containsString("GetNameById(1)"))) 274 | } @@ nonFlaky(10), 275 | suite("race")( 276 | test("race with never") { 277 | val query = ZQuery.never.race(ZQuery.succeed(())) 278 | assertZIO(query.run)(anything) 279 | }, 280 | test("interruption of loser") { 281 | for { 282 | promise1 <- Promise.make[Nothing, Unit] 283 | promise2 <- Promise.make[Nothing, Unit] 284 | left = ZQuery.fromZIO((promise1.succeed(()) *> ZIO.never).onInterrupt(promise2.succeed(()))) 285 | right = ZQuery.fromZIO(promise1.await) 286 | _ <- left.race(right).run 287 | _ <- promise2.await 288 | } yield assertCompletes 289 | } 290 | ) @@ nonFlaky, 291 | suite("around data source aspect")( 292 | test("wraps data source with before and after effects that are evaluated accordingly") { 293 | for { 294 | beforeRef <- Ref.make(0) 295 | before = beforeRef.set(1) *> beforeRef.get 296 | 297 | afterRef <- Ref.make(0) 298 | after = (v: Int) => afterRef.set(v * 2) 299 | aspect = QueryAspect.aroundDataSource(Described(before, "before effect"))(Described(after, "after effect")) 300 | query = getUserNameById(1) @@ aspect 301 | _ <- query.run 302 | isBeforeRan <- beforeRef.get 303 | isAfterRan <- afterRef.get 304 | } yield assert(isBeforeRan)(equalTo(1)) && assert(isAfterRan)(equalTo(2)) 305 | } 306 | ) @@ nonFlaky, 307 | test("service methods works with multiple services") { 308 | def getFoo: ZQuery[Int with String, Nothing, Unit] = 309 | ZQuery.serviceWithQuery[Int](_ => ZQuery.service[String].as(())) 310 | 311 | def getBar: ZQuery[Int with String, Nothing, Unit] = 312 | ZQuery.serviceWithZIO[String](_ => ZIO.service[String].unit) 313 | 314 | assertCompletes 315 | }, 316 | test("acquireReleaseWith") { 317 | def query(n: Int): ZQuery[Cache, Nothing, Unit] = 318 | if (n == 0) ZQuery.unit 319 | else ZQuery.fromZIO(Random.nextInt).flatMap(Cache.get(_).flatMap(_ => query(n - 1))) 320 | for { 321 | ref <- Ref.make(0) 322 | acquire = ref.update(_ + 1) 323 | release = ref.update(_ - 1) 324 | fiber <- ZQuery 325 | .acquireReleaseWith(acquire)(_ => release)(_ => query(100)) 326 | .run 327 | .fork 328 | _ <- fiber.interrupt 329 | value <- ref.get 330 | } yield assertTrue(value == 0) 331 | }.provideLayer(Cache.live) @@ nonFlaky, 332 | test("defect in data source is translated to defect in request") { 333 | val query = dieQuery.foldCauseQuery(_ => ZQuery.unit, _ => ZQuery.unit) 334 | for { 335 | _ <- query.run 336 | } yield assertCompletes 337 | }, 338 | suite("memoize")( 339 | test("results are memoized") { 340 | for { 341 | ref <- Ref.make(0) 342 | query <- ZQuery.fromZIO(ref.update(_ + 1)).memoize 343 | _ <- (ZQuery.foreachPar((1 to 100).toList)(_ => query) <* query).run 344 | value <- ref.get 345 | } yield assertTrue(value == 1) 346 | }, 347 | test("results are not computed when the outer effect is executed") { 348 | for { 349 | ref <- Ref.make(0) 350 | query <- ZQuery.fromZIO(ref.update(_ + 1)).memoize 351 | value <- ref.get 352 | } yield assertTrue(value == 0) 353 | }, 354 | test("errors are memoized") { 355 | for { 356 | ref <- Ref.make(0) 357 | query <- ZQuery.fromZIO(ref.updateAndGet(_ + 1).flatMap(i => ZIO.fail(new Exception(i.toString)))).memoize 358 | results <- ZQuery.foreachPar((1 to 100).toList)(_ => query.either).run 359 | value <- ref.get 360 | } yield assertTrue(value == 1, results.forall(_.isLeft)) 361 | }, 362 | test("defects are memoized") { 363 | for { 364 | ref <- Ref.make(0) 365 | query <- ZQuery.fromZIO(ref.updateAndGet(_ + 1).flatMap(i => ZIO.die(new Exception(i.toString)))).memoize 366 | results <- ZQuery 367 | .foreachPar((1 to 100).toList)(_ => 368 | query.foldCauseQuery(c => ZQuery.succeed(Left(c)), v => ZQuery.succeed(Right(v))) 369 | ) 370 | .run 371 | value <- ref.get 372 | } yield assertTrue(value == 1, results.forall(_.isLeft)) 373 | } 374 | ), 375 | suite("run")( 376 | test("cache is reentrant safe") { 377 | val q = 378 | for { 379 | c1 <- ZQuery.fromZIO(ZQuery.currentCache.get) 380 | _ <- ZQuery.fromZIO(ZQuery.succeed("foo").run) 381 | c2 <- ZQuery.fromZIO(ZQuery.currentCache.get) 382 | } yield (c1, c2) 383 | 384 | q.run.map { case (c1, c2) => assertTrue(c1.isDefined, c1 == c2) } 385 | }, 386 | test("disabling caching is reentrant safe") { 387 | val q = 388 | for { 389 | c1 <- ZQuery.fromZIO(ZQuery.currentCache.get) 390 | c2 <- ZQuery.fromZIO(ZQuery.fromZIO(ZQuery.currentCache.get).cached.run).uncached 391 | c3 <- ZQuery.fromZIO(ZQuery.currentCache.get) 392 | } yield (c1, c2, c3) 393 | 394 | q.run.map { case (c1, c2, c3) => assertTrue(c1.isDefined, c2.isDefined, c1 == c3, c1 != c2) } 395 | }, 396 | test("scope is reentrant safe") { 397 | val q = 398 | for { 399 | c1 <- ZQuery.fromZIO(ZQuery.currentScope.get) 400 | _ <- ZQuery.fromZIO(ZQuery.succeed("foo").run) 401 | c2 <- ZQuery.fromZIO(ZQuery.currentScope.get) 402 | } yield (c1, c2) 403 | 404 | q.run.map { case (c1, c2) => assertTrue(c1 != QueryScope.NoOp, c1 == c2) } 405 | }, 406 | test("propagates FiberRef changes") { 407 | val ref = FiberRef.unsafe.make("a")(Unsafe) 408 | for { 409 | _ <- ZQuery.fromZIO(ref.update(_ + "b")).run 410 | res1 <- ref.get 411 | _ <- ZQuery.fromZIO(ref.update(_ + "c")).run 412 | res2 <- ref.get 413 | _ <- ref.set("d") 414 | _ <- ZQuery.fromZIO(ref.update(_ + "e")).run 415 | res3 <- ref.get 416 | } yield assertTrue(res1 == "ab", res2 == "abc", res3 == "de") 417 | } 418 | ), 419 | suite("catchAllZIO")( 420 | test("doesn't affect successful value") { 421 | val query = (ZQuery.succeed(1): TaskQuery[Int]).catchAllZIO(_ => Exit.succeed(100)) 422 | for { 423 | res <- query.run 424 | } yield assertTrue(res == 1) 425 | }, 426 | test("catches failure") { 427 | val q: ZQuery[Any, Int, Int] = ZQuery.fail(1) 428 | val query = q.catchAllZIO(i => Exit.succeed(i + 1)) 429 | for { 430 | res <- query.run 431 | } yield assertTrue(res == 2) 432 | }, 433 | test("doesn't catch defects") { 434 | val query = (dieQuery: TaskQuery[Int]).catchAllZIO(_ => ZIO.succeed(1)) 435 | for { 436 | res <- query.run.cause 437 | } yield assertTrue(res.isDie) 438 | } 439 | ), 440 | suite("catchAllCauseZIO")( 441 | test("doesn't affect successful value") { 442 | val query = (ZQuery.succeed(1): TaskQuery[Int]).catchAllCauseZIO(_ => Exit.succeed(100)) 443 | for { 444 | res <- query.run 445 | } yield assertTrue(res == 1) 446 | }, 447 | test("catches failure") { 448 | val q: ZQuery[Any, Int, Int] = ZQuery.fail(1) 449 | val query = q.catchAllCauseZIO(_.failureOrCause.fold(i => Exit.succeed(i + 1), Exit.failCause)) 450 | for { 451 | res <- query.run 452 | } yield assertTrue(res == 2) 453 | }, 454 | test("catches defects") { 455 | val query = (dieQuery: TaskQuery[Int]).catchAllCauseZIO(_ => ZIO.succeed(1)) 456 | for { 457 | res <- query.run 458 | } yield assertTrue(res == 1) 459 | } 460 | ), 461 | suite("foldZIO")( 462 | test("maps success value") { 463 | val query = ZQuery.succeed(1).foldZIO(_ => Exit.succeed(1), i => Exit.succeed(i + 10)) 464 | for { 465 | res <- query.run 466 | } yield assertTrue(res == 11) 467 | }, 468 | test("catches failure") { 469 | val q: ZQuery[Any, Int, Int] = ZQuery.fail(1) 470 | val query = q.foldZIO(i => Exit.succeed(i + 1), i => Exit.succeed(i + 10)) 471 | for { 472 | res <- query.run 473 | } yield assertTrue(res == 2) 474 | }, 475 | test("doesn't catch defects") { 476 | val query = dieQuery.foldZIO(_ => Exit.succeed(1), _ => Exit.succeed(2)) 477 | for { 478 | res <- query.run.cause 479 | } yield assertTrue(res.isDie) 480 | } 481 | ), 482 | suite("foldCauseZIO")( 483 | test("maps success value") { 484 | val query = ZQuery.succeed(1).foldCauseZIO(_ => Exit.succeed(1), i => Exit.succeed(i + 10)) 485 | for { 486 | res <- query.run 487 | } yield assertTrue(res == 11) 488 | }, 489 | test("catches failure") { 490 | val q: ZQuery[Any, Int, Int] = ZQuery.fail(1) 491 | val query = 492 | q.foldCauseZIO(i => Exit.succeed(i.failureOrCause.left.toOption.get + 1), i => Exit.succeed(i + 10)) 493 | for { 494 | res <- query.run 495 | } yield assertTrue(res == 2) 496 | }, 497 | test("catches defect") { 498 | val query = dieQuery.foldCauseZIO(_ => Exit.succeed(1), _ => Exit.succeed(2)) 499 | for { 500 | res <- query.run 501 | } yield assertTrue(res == 1) 502 | } 503 | ) 504 | ) @@ silent 505 | 506 | val userIds: List[Int] = (1 to 26).toList 507 | val userNames: Map[Int, String] = userIds.zip(('a' to 'z').map(_.toString)).toMap 508 | 509 | sealed trait UserRequest[A] extends Request[Nothing, A] 510 | 511 | case object GetAllIds extends UserRequest[List[Int]] 512 | final case class GetNameById(id: Int) extends UserRequest[String] 513 | 514 | val UserRequestDataSource: DataSource[Any, UserRequest[_]] = 515 | DataSource.Batched.make[Any, UserRequest[_]]("UserRequestDataSource") { requests => 516 | (ZIO.when(requests.toSet.size != requests.size)(ZIO.dieMessage("Duplicate requests)")) *> 517 | Console.printLine(requests.toString).orDie).as { 518 | requests.foldLeft(CompletedRequestMap.empty) { 519 | case (completedRequests, GetAllIds) => completedRequests.insert(GetAllIds, Exit.succeed(userIds)) 520 | case (completedRequests, GetNameById(id)) => 521 | userNames 522 | .get(id) 523 | .fold(completedRequests)(name => completedRequests.insert(GetNameById(id), Exit.succeed(name))) 524 | } 525 | } 526 | } 527 | 528 | val getAllUserIds: ZQuery[Any, Nothing, List[Int]] = 529 | ZQuery.fromRequest(GetAllIds)(UserRequestDataSource) 530 | 531 | def getUserNameById(id: Int): ZQuery[Any, Nothing, String] = 532 | ZQuery.fromRequest(GetNameById(id))(UserRequestDataSource) 533 | 534 | val getAllUserNames: ZQuery[Any, Nothing, List[String]] = 535 | for { 536 | userIds <- getAllUserIds 537 | userNames <- ZQuery.fromRequestsWith(userIds, GetNameById(_))(UserRequestDataSource) 538 | } yield userNames 539 | 540 | case object GetFoo extends Request[Nothing, String] 541 | val getFoo: ZQuery[Any, Nothing, String] = ZQuery.fromRequest(GetFoo)( 542 | DataSource.fromFunctionZIO("foo")(_ => Console.printLine("Running foo query").orDie.as("foo")) 543 | ) 544 | 545 | case object GetBar extends Request[Nothing, String] 546 | val getBar: ZQuery[Any, Nothing, String] = ZQuery.fromRequest(GetBar)( 547 | DataSource.fromFunctionZIO("bar")(_ => Console.printLine("Running bar query").orDie.as("bar")) 548 | ) 549 | 550 | case object NeverRequest extends Request[Nothing, Nothing] 551 | 552 | val neverQuery: ZQuery[Any, Nothing, Nothing] = 553 | ZQuery.fromRequest(NeverRequest)(DataSource.never) 554 | 555 | final case class SucceedRequest(promise: Promise[Nothing, Unit]) extends Request[Nothing, Unit] 556 | 557 | val succeedDataSource: DataSource[Any, SucceedRequest] = 558 | DataSource.fromFunctionZIO("succeed") { case SucceedRequest(promise) => 559 | promise.succeed(()).unit 560 | } 561 | 562 | def succeedQuery(promise: Promise[Nothing, Unit]): ZQuery[Any, Nothing, Unit] = 563 | ZQuery.fromRequest(SucceedRequest(promise))(succeedDataSource) 564 | 565 | val raceDataSource: DataSource[Any, SucceedRequest] = 566 | DataSource.never.race(succeedDataSource) 567 | 568 | def raceQuery(promise: Promise[Nothing, Unit]): ZQuery[Any, Nothing, Unit] = 569 | ZQuery.fromRequest(SucceedRequest(promise))(raceDataSource) 570 | 571 | case object DieRequest extends Request[Nothing, Nothing] 572 | 573 | val dieDataSource: DataSource[Any, DieRequest.type] = 574 | new DataSource[Any, DieRequest.type] { 575 | val identifier: String = "die" 576 | 577 | def runAll(requests: Chunk[Chunk[DieRequest.type]])(implicit 578 | trace: Trace 579 | ): ZIO[Any, Nothing, CompletedRequestMap] = 580 | ZIO.dieMessage("die") 581 | } 582 | 583 | val dieQuery: ZQuery[Any, Nothing, Nothing] = 584 | ZQuery.fromRequest(DieRequest)(dieDataSource) 585 | 586 | sealed trait CacheRequest[A] extends Request[Nothing, A] 587 | 588 | final case class Get(key: Int) extends CacheRequest[Option[Int]] 589 | case object GetAll extends CacheRequest[Map[Int, Int]] 590 | final case class Put(key: Int, value: Int) extends CacheRequest[Unit] 591 | final case class PutIfAbsent(key: Int, value: Int) extends CacheRequest[Unit] 592 | 593 | type Cache = Cache.Service 594 | 595 | object Cache { 596 | 597 | trait Service extends DataSource[Any, CacheRequest[_]] { 598 | val clear: ZIO[Any, Nothing, Unit] 599 | val log: ZIO[Any, Nothing, List[List[Set[CacheRequest[_]]]]] 600 | } 601 | 602 | val live: ZLayer[Any, Throwable, Cache] = 603 | ZLayer.fromZIO { 604 | for { 605 | cache <- Ref.make(Map.empty[Int, Int]) 606 | ref <- Ref.make[List[List[Set[CacheRequest[_]]]]](Nil) 607 | } yield new Service { 608 | val clear: ZIO[Any, Nothing, Unit] = 609 | cache.set(Map.empty) *> ref.set(List.empty) 610 | val log: ZIO[Any, Nothing, List[List[Set[CacheRequest[_]]]]] = 611 | ref.get 612 | val identifier: String = 613 | "CacheDataSource" 614 | def runAll(requests: Chunk[Chunk[CacheRequest[_]]])(implicit 615 | trace: Trace 616 | ): ZIO[Any, Nothing, CompletedRequestMap] = 617 | ref.update(requests.map(_.toSet).toList :: _) *> 618 | ZIO 619 | .foreach(requests) { requests => 620 | ZIO 621 | .foreachPar(requests) { 622 | case Get(key) => 623 | cache.get.map(_.get(key)).exit.map(CompletedRequestMap.empty.insert(Get(key), _)) 624 | case GetAll => 625 | cache.get.exit.map(CompletedRequestMap.empty.insert(GetAll, _)) 626 | case Put(key, value) => 627 | cache.update(_ + (key -> value)).exit.map(CompletedRequestMap.empty.insert(Put(key, value), _)) 628 | case PutIfAbsent(key, value) => 629 | cache.get.flatMap { map => 630 | if (map.contains(key)) ZIO.die(new Exception(s"Expected key $key to be absent from cache")) 631 | else 632 | cache 633 | .update(_ + (key -> value)) 634 | .exit 635 | .map(CompletedRequestMap.empty.insert(PutIfAbsent(key, value), _)) 636 | } 637 | } 638 | .map(_.foldLeft(CompletedRequestMap.empty)(_ ++ _)) 639 | } 640 | .map(_.foldLeft(CompletedRequestMap.empty)(_ ++ _)) 641 | } 642 | } 643 | 644 | def get(key: Int): ZQuery[Cache, Nothing, Option[Int]] = 645 | for { 646 | cache <- ZQuery.environment[Cache].map(_.get) 647 | value <- ZQuery.fromRequest(Get(key))(cache) 648 | } yield value 649 | 650 | val getAll: ZQuery[Cache, Nothing, Map[Int, Int]] = 651 | for { 652 | cache <- ZQuery.environment[Cache].map(_.get) 653 | value <- ZQuery.fromRequest(GetAll)(cache) 654 | } yield value 655 | 656 | def put(key: Int, value: Int): ZQuery[Cache, Nothing, Unit] = 657 | for { 658 | cache <- ZQuery.environment[Cache].map(_.get) 659 | value <- ZQuery.fromRequest(Put(key, value))(cache) 660 | } yield value 661 | 662 | def putIfAbsent(key: Int, value: Int): ZQuery[Cache, Nothing, Unit] = 663 | for { 664 | cache <- ZQuery.environment[Cache].map(_.get) 665 | value <- ZQuery.fromRequest(PutIfAbsent(key, value))(cache) 666 | } yield value 667 | 668 | val clear: ZIO[Cache, Nothing, Unit] = 669 | ZIO.serviceWithZIO(_.clear) 670 | 671 | val log: ZIO[Cache, Nothing, List[List[Set[CacheRequest[_]]]]] = 672 | ZIO.serviceWithZIO(_.log) 673 | } 674 | 675 | case class Bearer(value: String) 676 | 677 | case class User(id: Int, name: String, addressId: Int, paymentId: Int) 678 | case class Address(id: Int, street: String) 679 | case class Payment(id: Int, name: String) 680 | 681 | object Sources { 682 | 683 | val totalCount = 15000 684 | 685 | val paymentData: Map[Int, Payment] = List.tabulate(totalCount)(i => i -> Payment(i, "payment name")).toMap 686 | case class GetPayment(id: Int) extends Request[Nothing, Payment] 687 | val paymentSource: DataSource[Any, GetPayment] = 688 | DataSource.fromFunctionBatchedOptionZIO("PaymentSource") { (requests: Chunk[GetPayment]) => 689 | ZIO.succeed(requests.map(req => paymentData.get(req.id))) 690 | } 691 | 692 | def getPayment(id: Int): UQuery[Payment] = 693 | ZQuery.fromRequest(GetPayment(id))(paymentSource) 694 | 695 | val addressData: Map[Int, Address] = List.tabulate(totalCount)(i => i -> Address(i, "street")).toMap 696 | case class GetAddress(id: Int) extends Request[Nothing, Address] 697 | val addressSource: DataSource[Any, GetAddress] = 698 | DataSource.fromFunctionBatchedOptionZIO("AddressSource") { (requests: Chunk[GetAddress]) => 699 | ZIO.succeed(requests.map(req => addressData.get(req.id))) 700 | } 701 | 702 | def getAddress(id: Int): UQuery[Address] = 703 | ZQuery.fromRequest(GetAddress(id))(addressSource) 704 | } 705 | 706 | val testData: Map[Int, String] = Map( 707 | 1 -> "a", 708 | 2 -> "b", 709 | 3 -> "c", 710 | 4 -> "d" 711 | ) 712 | 713 | def backendGetAll: ZIO[Any, Nothing, Map[Int, String]] = 714 | for { 715 | _ <- Console.printLine("getAll called").orDie 716 | } yield testData 717 | 718 | def backendGetSome(ids: Chunk[Int]): ZIO[Any, Nothing, Map[Int, String]] = 719 | for { 720 | _ <- Console.printLine(s"getSome ${ids.mkString(", ")} called").orDie 721 | } yield ids.flatMap { id => 722 | testData.get(id).map(v => id -> v) 723 | }.toMap 724 | 725 | sealed trait DataSourceErrors 726 | case class NotFound(id: Int) extends DataSourceErrors 727 | 728 | sealed trait Req[A] extends Request[DataSourceErrors, A] 729 | object Req { 730 | case object GetAll extends Req[Map[Int, String]] 731 | final case class Get(id: Int) extends Req[String] 732 | } 733 | 734 | val ds: DataSource.Batched[Any, Req[_]] = new DataSource.Batched[Any, Req[_]] { 735 | override def run( 736 | requests: Chunk[Req[_]] 737 | )(implicit trace: Trace): ZIO[Any, Nothing, CompletedRequestMap] = { 738 | val (all, oneByOne) = requests.partition { 739 | case Req.GetAll => true 740 | case Req.Get(_) => false 741 | } 742 | 743 | if (all.nonEmpty) { 744 | backendGetAll.map { allItems => 745 | allItems 746 | .foldLeft(CompletedRequestMap.empty) { case (result, (id, value)) => 747 | result.insert(Req.Get(id), Exit.succeed(value)) 748 | } 749 | .insert(Req.GetAll, Exit.succeed(allItems)) 750 | } 751 | } else { 752 | for { 753 | items <- backendGetSome(oneByOne.flatMap { 754 | case Req.GetAll => Chunk.empty 755 | case Req.Get(id) => Chunk(id) 756 | }) 757 | } yield oneByOne.foldLeft(CompletedRequestMap.empty) { 758 | case (result, Req.GetAll) => result 759 | case (result, req @ Req.Get(id)) => 760 | items.get(id) match { 761 | case Some(value) => result.insert(req, Exit.succeed(value)) 762 | case None => result.insert(req, Exit.fail(NotFound(id))) 763 | } 764 | } 765 | } 766 | } 767 | 768 | override val identifier: String = "test" 769 | } 770 | 771 | val dsCompletingMoreRequests: DataSource.Batched[Any, Req.Get] = new DataSource.Batched[Any, Req.Get] { 772 | override def run( 773 | requests: Chunk[Req.Get] 774 | )(implicit trace: Trace): ZIO[Any, Nothing, CompletedRequestMap] = 775 | ZIO.succeed( 776 | CompletedRequestMap.fromIterable( 777 | (1 to 10).map(id => Req.Get(id) -> Exit.succeed(id.toString)) 778 | ) 779 | ) 780 | 781 | override val identifier: String = "test" 782 | } 783 | 784 | def getAll: ZQuery[Any, DataSourceErrors, Map[Int, String]] = 785 | ZQuery.fromRequest(Req.GetAll)(ds) 786 | def get(id: Int): ZQuery[Any, DataSourceErrors, String] = 787 | ZQuery.fromRequest(Req.Get(id))(ds) 788 | } 789 | --------------------------------------------------------------------------------