
skunk-core_3.0.0-RC1.0.1.0.source-code.PreparedQuery.scala Maven / Gradle / Ivy
// Copyright (c) 2018-2021 by Rob Norris
// This software is licensed under the MIT License (MIT).
// For more information see LICENSE or https://opensource.org/licenses/MIT
package skunk
import cats._
import cats.arrow.Profunctor
import cats.effect._
import cats.syntax.all._
import fs2.{ Chunk, Stream, Pipe }
import skunk.exception.SkunkException
import skunk.net.Protocol
import skunk.util.{ CallSite, Origin }
/**
* A prepared query, valid for the life of its originating `Session`.
* @group Session
*/
trait PreparedQuery[F[_], A, B] {
/**
* `Resource` that binds the supplied arguments to this `PreparedQuery`, yielding a `Cursor` from
* which rows can be `fetch`ed. Note that higher-level operations like `stream`, `option`, and
* `unique` are usually what you want.
*/
def cursor(args: A)(implicit or: Origin): Resource[F, Cursor[F, B]]
/**
* Construct a `Cursor`-backed stream that calls `fetch` repeatedly and emits chunks until none
* remain. Note that each chunk is read atomically while holding the session mutex, which means
* interleaved streams will achieve better fairness with smaller chunks but greater overall
* throughput with larger chunks. So it's important to consider the use case when specifying
* `chunkSize`.
*/
def stream(args: A, chunkSize: Int)(implicit or: Origin): Stream[F, B]
/**
* Fetch and return at most one row, raising an exception if more rows are available.
*/
def option(args: A)(implicit or: Origin): F[Option[B]]
/**
* Fetch and return exactly one row, raising an exception if there are more or fewer.
*/
def unique(args: A)(implicit or: Origin): F[B]
/**
* A `Pipe` that executes this `PreparedQuery` for each input value, concatenating the resulting
* streams. See `stream` for details on the `chunkSize` parameter.
*/
def pipe(chunkSize: Int)(implicit or: Origin): Pipe[F, A, B] =
_.flatMap(stream(_, chunkSize))
}
/** @group Companions */
object PreparedQuery {
def fromProto[F[_], A, B](proto: Protocol.PreparedQuery[F, A, B])(
implicit ev: MonadCancel[F, Throwable]
): PreparedQuery[F, A, B] =
new PreparedQuery[F, A, B] {
override def cursor(args: A)(implicit or: Origin): Resource[F, Cursor[F, B]] =
proto.bind(args, or).map { p =>
new Cursor[F, B] {
override def fetch(maxRows: Int): F[(List[B], Boolean)] =
p.execute(maxRows)
}
}
override def stream(args: A, chunkSize: Int)(implicit or: Origin): Stream[F, B] =
Stream.resource(proto.bind(args, or)).flatMap { cursor =>
def chunks: Stream[F, B] =
Stream.eval(cursor.execute(chunkSize)).flatMap { case (bs, more) =>
val s = Stream.chunk(Chunk.seq(bs))
if (more) s ++ chunks
else s
}
chunks
}
// We have a few operations that only want the first row. In order to do this AND
// know if there are more we need to ask for 2 rows.
private def fetch2(args: A)(implicit or: Origin): F[(List[B], Boolean)] =
cursor(args).use(_.fetch(2))
override def option(args: A)(implicit or: Origin): F[Option[B]] =
fetch2(args).flatMap { case (bs, _) =>
bs match {
case b :: Nil => b.some.pure[F]
case Nil => none[B].pure[F]
case _ =>
fail("option", args, or,
"Expected at most one result, more returned.",
s"Did you mean to use ${framed("stream")}?"
)
}
}
override def unique(args: A)(implicit or: Origin): F[B] =
fetch2(args).flatMap { case (bs, _) =>
bs match {
case b :: Nil => b.pure[F]
case Nil =>
fail(
method = "unique",
args = args,
or = or,
m = "Exactly one row was expected, but none were returned.",
h = s"You used ${framed("unique")}. Did you mean to use ${framed("option")}?"
)
case _ =>
fail("unique", args, or,
"Exactly one row was expected, but more were returned.",
s"You used ${framed("unique")}. Did you mean to use ${framed("stream")}?"
)
}
}
private def framed(s: String): String =
"\u001B[4m" + s + "\u001B[24m"
private def fail[T](method: String, args: A, or: Origin, m: String, h: String): F[T] =
MonadError[F, Throwable].raiseError[T] {
SkunkException.fromQueryAndArguments(
message = m,
query = proto.query,
args = args,
callSite = Some(CallSite(method, or)),
hint = Some(h),
argsOrigin = Some(or)
)
}
}
/**
* `PreparedQuery[F, *, B]` is a covariant functor when `F` is a monad.
* @group Typeclass Instances
*/
implicit def functorPreparedQuery[F[_]: Monad, A]: Functor[PreparedQuery[F, A, *]] =
new Functor[PreparedQuery[F, A, *]] {
override def map[T, U](fa: PreparedQuery[F, A, T])(f: T => U): PreparedQuery[F, A, U] =
new PreparedQuery[F, A, U] {
override def cursor(args: A)(implicit or: Origin): Resource[F, Cursor[F, U]] = fa.cursor(args).map(_.map(f))
override def stream(args: A, chunkSize: Int)(implicit or: Origin): Stream[F, U] = fa.stream(args, chunkSize).map(f)
override def option(args: A)(implicit or: Origin): F[Option[U]] = fa.option(args).map(_.map(f))
override def unique(args: A)(implicit or: Origin): F[U] = fa.unique(args).map(f)
}
}
/**
* `PreparedQuery[F, *, B]` is a contravariant functor for all `F`.
* @group Typeclass Instances
*/
implicit def contravariantPreparedQuery[F[_], B]: Contravariant[PreparedQuery[F, *, B]] =
new Contravariant[PreparedQuery[F, *, B]] {
override def contramap[T, U](fa: PreparedQuery[F, T, B])(f: U => T): PreparedQuery[F, U, B] =
new PreparedQuery[F, U, B] {
override def cursor(args: U)(implicit or: Origin): Resource[F, Cursor[F, B]] = fa.cursor(f(args))
override def stream(args: U, chunkSize: Int)(implicit or: Origin): Stream[F, B] = fa.stream(f(args), chunkSize)
override def option(args: U)(implicit or: Origin): F[Option[B]] = fa.option(f(args))
override def unique(args: U)(implicit or: Origin): F[B] = fa.unique(f(args))
}
}
/**
* `PreparedQuery[F, *, *]` is a profunctor when `F` is a monad.
* @group Typeclass Instances
*/
implicit def profunctorPreparedQuery[F[_]: Monad]: Profunctor[PreparedQuery[F, *, *]] =
new Profunctor[PreparedQuery[F, *, *]] {
override def dimap[A, B, C, D](fab: PreparedQuery[F, A, B])(f: C => A)(g: B => D): PreparedQuery[F, C, D] =
contravariantPreparedQuery[F, B].contramap(fab)(f).map(g) // y u no work contravariant syntax
override def lmap[A, B, C](fab: PreparedQuery[F, A, B])(f: C => A): PreparedQuery[F, C, B] =
contravariantPreparedQuery[F, B].contramap(fab)(f)
override def rmap[A, B, C](fab: PreparedQuery[F, A, B])(f: B => C): PreparedQuery[F, A, C] = fab.map(f)
}
implicit class PreparedQueryOps[F[_], A, B](outer: PreparedQuery[F, A, B])(
implicit mcf: MonadCancel[F, _]
) {
/**
* Transform this `PreparedQuery` by a given `FunctionK`.
* @group Transformations
*/
def mapK[G[_]](fk: F ~> G)(
implicit mcg: MonadCancel[G, _]
): PreparedQuery[G, A, B] =
new PreparedQuery[G, A, B] {
override def cursor(args: A)(implicit or: Origin): Resource[G,Cursor[G,B]] = outer.cursor(args).mapK(fk).map(_.mapK(fk))
override def option(args: A)(implicit or: Origin): G[Option[B]] = fk(outer.option(args))
override def stream(args: A, chunkSize: Int)(implicit or: Origin): Stream[G,B] = outer.stream(args, chunkSize).translate(fk)
override def unique(args: A)(implicit or: Origin): G[B] = fk(outer.unique(args))
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy