diff --git a/modules/core/src/main/scala/doobie/hi/connection.scala b/modules/core/src/main/scala/doobie/hi/connection.scala index ae02a2020..914f287f2 100644 --- a/modules/core/src/main/scala/doobie/hi/connection.scala +++ b/modules/core/src/main/scala/doobie/hi/connection.scala @@ -60,6 +60,9 @@ object connection { * * Errors at each step are handled and logged, with cleanups (closing the PreparedStatement/ResultSet.) * + * For usage example, see [[doobie.util.query.Query.to]] or [[doobie.util.update.Update.withUniqueGeneratedKeys]] + * which uses this function. + * * @param create * Create the PreparedStatement, using e.g. `doobie.FC.prepareStatement` * @param prep @@ -91,6 +94,7 @@ object connection { * * Errors at each step are handled and logged, with the PreparedStatement being closed at the end. * + * For usage examples, see [[doobie.util.update.Update.updateMany]] which is built on this function * @param create * Create the PreparedStatement, using e.g. `doobie.FC.prepareStatement` * @param prep @@ -174,10 +178,10 @@ object connection { .bracket(ps => IFC.embed(ps, prepLogged *> execAndProcessLogged))(ps => IFC.embed(ps, IFPS.close)) } - // FIXME: test PS and RS are correctly closed when there's exec error or processing error - /** Execute a PreparedStatement query and provide rows from the ResultSet in chunks * + * For usage examples, see [[doobie.util.query.Query.streamWithChunkSize]] which is implemented on top of this + * function. * @param create * Create the PreparedStatement, using e.g. `doobie.FC.prepareStatement` * @param prep diff --git a/modules/core/src/main/scala/doobie/hi/preparedstatement.scala b/modules/core/src/main/scala/doobie/hi/preparedstatement.scala index ecb8c2ef5..4dec53db2 100644 --- a/modules/core/src/main/scala/doobie/hi/preparedstatement.scala +++ b/modules/core/src/main/scala/doobie/hi/preparedstatement.scala @@ -111,7 +111,7 @@ object preparedstatement { /** @group Execution */ @deprecated( - "Consider using doobie.HC.execute{With/Without}ResultSet" + + "Consider using doobie.HC.executeWithoutResultSet" + "for logging support, or switch to executeUpdateWithUniqueGeneratedKeysUnlogged instead", "1.0.0-RC6" ) @@ -123,7 +123,7 @@ object preparedstatement { /** @group Execution */ @deprecated( - "Consider using doobie.HC.execute{With/Without}ResultSet" + + "Consider using doobie.HC.stream" + "for logging support, or switch to executeUpdateWithUniqueGeneratedKeysUnlogged instead", "1.0.0-RC6" ) diff --git a/modules/core/src/main/scala/doobie/util/query.scala b/modules/core/src/main/scala/doobie/util/query.scala index e18dfcce9..c17aaa993 100644 --- a/modules/core/src/main/scala/doobie/util/query.scala +++ b/modules/core/src/main/scala/doobie/util/query.scala @@ -10,7 +10,7 @@ import cats.data.NonEmptyList import doobie._ import doobie.util.analysis.Analysis import doobie.util.compat.FactoryCompat -import doobie.util.log.{Arguments, LoggingInfo} +import doobie.util.log.{Parameters, LoggingInfo} import doobie.util.pos.Pos import doobie.free.{preparedstatement => IFPS, connection => IFC} import doobie.hi.{connection => IHC, preparedstatement => IHPS, resultset => IHRS} @@ -84,7 +84,7 @@ object query { chunkSize = chunkSize, loggingInfo = LoggingInfo( sql = sql, - args = Arguments.NonBatch(Write[A].toList(a)), + params = Parameters.NonBatch(Write[A].toList(a)), label = label ) ) @@ -157,7 +157,7 @@ object query { private def mkLoggingInfo(a: A): LoggingInfo = LoggingInfo( sql = sql, - args = Arguments.NonBatch(write.toList(a)), + params = Parameters.NonBatch(write.toList(a)), label = label ) diff --git a/modules/core/src/main/scala/doobie/util/update.scala b/modules/core/src/main/scala/doobie/util/update.scala index 4edf2ff07..f11a5c5fd 100644 --- a/modules/core/src/main/scala/doobie/util/update.scala +++ b/modules/core/src/main/scala/doobie/util/update.scala @@ -12,7 +12,7 @@ import doobie.util.analysis.Analysis import doobie.util.pos.Pos import doobie.free.{connection => IFC, preparedstatement => IFPS} import doobie.hi.{connection => IHC, preparedstatement => IHPS, resultset => IHRS} -import doobie.util.log.{Arguments, LoggingInfo} +import doobie.util.log.{Parameters, LoggingInfo} import fs2.Stream import scala.Predef.genericArrayOps @@ -86,7 +86,7 @@ object update { IFPS.executeUpdate, LoggingInfo( sql, - Arguments.NonBatch(write.toList(a)), + Parameters.NonBatch(write.toList(a)), label ) ) @@ -107,7 +107,7 @@ object update { exec = IFPS.executeBatch.map(updateCounts => updateCounts.foldLeft(0)((acc, n) => acc + (n.max(0)))), loggingInfo = LoggingInfo( sql, - Arguments.Batch(() => fa.toList.map(write.toList)), + Parameters.Batch(() => fa.toList.map(write.toList)), label ) ) @@ -130,7 +130,7 @@ object update { chunkSize = chunkSize, loggingInfo = LoggingInfo( sql, - Arguments.Batch(() => as.toList.map(Write[A].toList)), + Parameters.Batch(() => as.toList.map(Write[A].toList)), label ) ) @@ -157,7 +157,7 @@ object update { chunkSize = chunkSize, loggingInfo = LoggingInfo( sql = sql, - args = Arguments.NonBatch(Write[A].toList(a)), + params = Parameters.NonBatch(Write[A].toList(a)), label = label ) ) @@ -175,7 +175,7 @@ object update { process = IHRS.getUnique, loggingInfo = LoggingInfo( sql, - Arguments.NonBatch(write.toList(a)), + Parameters.NonBatch(write.toList(a)), label ) ) diff --git a/modules/core/src/test/scala-2/doobie/util/QueryLogSuitePlatform.scala b/modules/core/src/test/scala-2/doobie/util/QueryLogSuitePlatform.scala index 68720aaf9..b82855515 100644 --- a/modules/core/src/test/scala-2/doobie/util/QueryLogSuitePlatform.scala +++ b/modules/core/src/test/scala-2/doobie/util/QueryLogSuitePlatform.scala @@ -4,7 +4,7 @@ package doobie.util -import doobie.util.log.{Arguments, ProcessingFailure, Success} +import doobie.util.log.{Parameters, ProcessingFailure, Success} import shapeless._ trait QueryLogSuitePlatform { self: QueryLogSuite => @@ -14,8 +14,8 @@ trait QueryLogSuitePlatform { self: QueryLogSuite => val Sql = "select 1 where ? = ?" val Arg = 1 :: 1 :: HNil eventForUniqueQuery(Sql, Arg) match { - case Success(Sql, Arguments.NonBatch(List(1, 1)), _, _, _) => () - case a => fail(s"no match: $a") + case Success(Sql, Parameters.NonBatch(List(1, 1)), _, _, _) => () + case a => fail(s"no match: $a") } } @@ -23,8 +23,8 @@ trait QueryLogSuitePlatform { self: QueryLogSuite => val Sql = "select 1 where ? = ?" val Arg = 1 :: 2 :: HNil eventForUniqueQuery(Sql, Arg) match { - case ProcessingFailure(Sql, Arguments.NonBatch(List(1, 2)), _, _, _, _) => () - case a => fail(s"no match: $a") + case ProcessingFailure(Sql, Parameters.NonBatch(List(1, 2)), _, _, _, _) => () + case a => fail(s"no match: $a") } } diff --git a/modules/core/src/test/scala-3/doobie/util/QueryLogSuitePlatform.scala b/modules/core/src/test/scala-3/doobie/util/QueryLogSuitePlatform.scala index 6b995718a..8909c0270 100644 --- a/modules/core/src/test/scala-3/doobie/util/QueryLogSuitePlatform.scala +++ b/modules/core/src/test/scala-3/doobie/util/QueryLogSuitePlatform.scala @@ -4,7 +4,7 @@ package doobie.util -import doobie.util.log.{Arguments, Success, ProcessingFailure} +import doobie.util.log.{Parameters, Success, ProcessingFailure} trait QueryLogSuitePlatform { self: QueryLogSuite => import doobie.generic.auto._ @@ -13,7 +13,7 @@ trait QueryLogSuitePlatform { self: QueryLogSuite => val Sql = "select 1 where ? = ?" val Arg = 1 *: 1 *: EmptyTuple eventForUniqueQuery(Sql, Arg) match { - case Success(Sql, Arguments.NonBatch(List(1, 1)), _, _, _) => () + case Success(Sql, Parameters.NonBatch(List(1, 1)), _, _, _) => () case a => fail(s"no match: $a") } } @@ -22,7 +22,7 @@ trait QueryLogSuitePlatform { self: QueryLogSuite => val Sql = "select 1 where ? = ?" val Arg = 1 *: 2 *: EmptyTuple eventForUniqueQuery(Sql, Arg) match { - case ProcessingFailure(Sql, Arguments.NonBatch(List(1, 2)), _, _, _, _) => () + case ProcessingFailure(Sql, Parameters.NonBatch(List(1, 2)), _, _, _, _) => () case a => fail(s"no match: $a") } } diff --git a/modules/core/src/test/scala/doobie/util/QueryLogSuite.scala b/modules/core/src/test/scala/doobie/util/QueryLogSuite.scala index 58757766b..53fbee5bd 100644 --- a/modules/core/src/test/scala/doobie/util/QueryLogSuite.scala +++ b/modules/core/src/test/scala/doobie/util/QueryLogSuite.scala @@ -8,8 +8,8 @@ import cats.syntax.all._ import cats.effect.{IO, IOLocal} import doobie._ import doobie.implicits._ -import doobie.util.log.Arguments.NonBatch -import doobie.util.log.{Arguments, ExecFailure, LogEvent, ProcessingFailure, Success} +import doobie.util.log.Parameters.NonBatch +import doobie.util.log.{Parameters, ExecFailure, LogEvent, ProcessingFailure, Success} class QueryLogSuite extends munit.FunSuite with QueryLogSuitePlatform { @@ -69,7 +69,7 @@ class QueryLogSuite extends munit.FunSuite with QueryLogSuitePlatform { ) succEvents.foreach { succ => assertEquals(succ.sql, "select 1, 2") - assertEquals(succ.args, NonBatch(Nil)) + assertEquals(succ.params, NonBatch(Nil)) assertEquals(succ.label, "unlabeled") } } @@ -86,7 +86,7 @@ class QueryLogSuite extends munit.FunSuite with QueryLogSuitePlatform { ) succEvents.foreach { succ => assertEquals(succ.sql, "select ?, ?") - assertEquals(succ.args, NonBatch(List(1, "2"))) + assertEquals(succ.params, NonBatch(List(1, "2"))) assertEquals(succ.label, "mylabel") assert(succ.exec.toNanos > 0L) assert(succ.processing.toNanos > 0L) @@ -104,7 +104,7 @@ class QueryLogSuite extends munit.FunSuite with QueryLogSuitePlatform { execFailureEventForCIO(q.nel) ).foreach { ev => assertEquals(ev.sql, "select bad_column") - assertEquals(ev.args, Arguments.nonBatchEmpty) + assertEquals(ev.params, Parameters.nonBatchEmpty) assertEquals(ev.label, "unlabeled") assertEquals(ev.exec.toNanos, 0L) assert(ev.failure.getMessage.contains("not found")) @@ -122,7 +122,7 @@ class QueryLogSuite extends munit.FunSuite with QueryLogSuitePlatform { execFailureEventForCIO(q.nel("not_int")) ).foreach { ev => assertEquals(ev.sql, "select ? :: Int") - assertEquals(ev.args, NonBatch(List("not_int"))) + assertEquals(ev.params, NonBatch(List("not_int"))) assertEquals(ev.label, "unlabeled") assert(ev.exec.toNanos > 0L) assert(ev.failure.getMessage.contains("Data conversion error")) @@ -140,7 +140,7 @@ class QueryLogSuite extends munit.FunSuite with QueryLogSuitePlatform { processFailureEventForCIO(q.nel) ).foreach { ev => assertEquals(ev.sql, "select 'not_int'") - assertEquals(ev.args, Arguments.nonBatchEmpty) + assertEquals(ev.params, Parameters.nonBatchEmpty) assertEquals(ev.label, "unlabeled") assert(ev.exec.toNanos > 0L) assert(ev.failure.getMessage.contains("Data conversion error")) @@ -153,7 +153,7 @@ class QueryLogSuite extends munit.FunSuite with QueryLogSuitePlatform { Query[Unit, Int](sql).stream(()).compile.toList ) assertEquals(succ.sql, sql) - assertEquals(succ.args, NonBatch(Nil)) + assertEquals(succ.params, NonBatch(Nil)) assertEquals(succ.label, "unlabeled") assert(succ.exec.toNanos > 0L) assertEquals(succ.processing.toNanos, 0L) @@ -165,7 +165,7 @@ class QueryLogSuite extends munit.FunSuite with QueryLogSuitePlatform { Query[Unit, Int](sql).streamWithChunkSize((), 5).compile.toList ) assertEquals(succ.sql, sql) - assertEquals(succ.args, NonBatch(Nil)) + assertEquals(succ.params, NonBatch(Nil)) assertEquals(succ.label, "unlabeled") assert(succ.exec.toNanos > 0L) assertEquals(succ.processing.toNanos, 0L) @@ -178,7 +178,7 @@ class QueryLogSuite extends munit.FunSuite with QueryLogSuitePlatform { execFailureEventForCIO(q0.streamWithChunkSize(1).compile.toList) ).foreach { ev => assertEquals(ev.sql, "select bad_column") - assertEquals(ev.args, Arguments.nonBatchEmpty) + assertEquals(ev.params, Parameters.nonBatchEmpty) assertEquals(ev.label, "unlabeled") assertEquals(ev.exec.toNanos, 0L) assert(ev.failure.getMessage.contains("not found")) @@ -192,7 +192,7 @@ class QueryLogSuite extends munit.FunSuite with QueryLogSuitePlatform { execFailureEventForCIO(q0.streamWithChunkSize("not_int", 1).compile.toList) ).foreach { ev => assertEquals(ev.sql, "select ? :: Int") - assertEquals(ev.args, NonBatch(List("not_int"))) + assertEquals(ev.params, NonBatch(List("not_int"))) assertEquals(ev.label, "unlabeled") assert(ev.exec.toNanos > 0L) assert(ev.failure.getMessage.contains("Data conversion error")) diff --git a/modules/core/src/test/scala/doobie/util/UpdateLogSuite.scala b/modules/core/src/test/scala/doobie/util/UpdateLogSuite.scala index 3f14983d0..fd42a2eba 100644 --- a/modules/core/src/test/scala/doobie/util/UpdateLogSuite.scala +++ b/modules/core/src/test/scala/doobie/util/UpdateLogSuite.scala @@ -8,7 +8,7 @@ import cats.effect.{IO, IOLocal} import cats.syntax.all._ import doobie._ import doobie.implicits._ -import doobie.util.log.Arguments.NonBatch +import doobie.util.log.Parameters.NonBatch import doobie.util.log._ import scala.annotation.nowarn @@ -63,7 +63,7 @@ class UpdateLogSuite extends munit.FunSuite { val cio = sql"INSERT INTO foo VALUES (${1}, ${"str"})".update.run val ev = successEventForCIO(cio) assertEquals(ev.sql, "INSERT INTO foo VALUES (?, ?)") - assertEquals(ev.args, NonBatch(List(1, "str"))) + assertEquals(ev.params, NonBatch(List(1, "str"))) assertEquals(ev.label, "unlabeled") } @@ -71,7 +71,7 @@ class UpdateLogSuite extends munit.FunSuite { val ev = execFailureEventForCIO(sql"insert into hm".update.run) assertEquals(ev.sql, "insert into hm") - assertEquals(ev.args, Arguments.nonBatchEmpty) + assertEquals(ev.params, Parameters.nonBatchEmpty) assertEquals(ev.label, "unlabeled") assertEquals(ev.exec.toNanos, 0L) assert(ev.failure.getMessage.contains("""Table "HM" not found""")) @@ -83,7 +83,7 @@ class UpdateLogSuite extends munit.FunSuite { val ev = execFailureEventForCIO(cio) assertEquals(ev.sql, "insert into foo values (?, ?)") - assertEquals(ev.args, NonBatch(List("s", "k"))) + assertEquals(ev.params, NonBatch(List("s", "k"))) assertEquals(ev.label, "unlabeled") assert(ev.exec.toNanos > 0L) assert(ev.failure.getMessage.contains("Data conversion error")) @@ -94,9 +94,9 @@ class UpdateLogSuite extends munit.FunSuite { .updateMany(List((1, 2), (3, 4), (5, 6))) val ev = successEventForCIO(cio) assertEquals(ev.sql, "INSERT INTO foo VALUES (?, ?)") - ev.args match { + ev.params match { case NonBatch(_) => fail("Expect batched args") - case Arguments.Batch(argsAsLists) => + case Parameters.Batch(argsAsLists) => assertEquals(argsAsLists(), List(List(1, 2), List(3, 4), List(5, 6))) } assertEquals(ev.label, "some_label") @@ -106,7 +106,7 @@ class UpdateLogSuite extends munit.FunSuite { test("updateMany: Log ExecFailure on failed PreparedStatement construction") { val ev = execFailureEventForCIO(Update[Int]("insert into hm").updateMany(List(1, 2))) assertEquals(ev.sql, "insert into hm") - assertEquals(ev.args.allArgs, List(List(1), List(2))) + assertEquals(ev.params.allParams, List(List(1), List(2))) assertEquals(ev.label, "unlabeled") assertEquals(ev.exec.toNanos, 0L) assert(ev.failure.getMessage.contains("""Table "HM" not found""")) @@ -118,7 +118,7 @@ class UpdateLogSuite extends munit.FunSuite { val ev = execFailureEventForCIO(cio) assertEquals(ev.sql, "insert into foo values (?, ?)") - assertEquals(ev.args.allArgs, List(List("s", "k"))) + assertEquals(ev.params.allParams, List(List("s", "k"))) assertEquals(ev.label, "unlabeled") assert(ev.exec.toNanos > 0L) } @@ -132,7 +132,7 @@ class UpdateLogSuite extends munit.FunSuite { val ev = successEventForCIO(cio) assertEquals(ev.sql, "insert into foo values (?, ?)") - assertEquals(ev.args.allArgs, List(List(1, "a"), List(2, "b"))) + assertEquals(ev.params.allParams, List(List(1, "a"), List(2, "b"))) assertEquals(ev.label, "unlabeled") assert(ev.exec.toNanos > 0L) assertEquals(ev.processing.toNanos, 0L) @@ -147,7 +147,7 @@ class UpdateLogSuite extends munit.FunSuite { val ev = execFailureEventForCIO(cio) assertEquals(ev.sql, "insert into foo") - assertEquals(ev.args.allArgs, List(List(1, "a"), List(2, "b"))) + assertEquals(ev.params.allParams, List(List(1, "a"), List(2, "b"))) assertEquals(ev.label, "unlabeled") assertEquals(ev.exec.toNanos, 0L) assert(ev.failure.getMessage.contains("Syntax error")) @@ -161,7 +161,7 @@ class UpdateLogSuite extends munit.FunSuite { val ev = execFailureEventForCIO(cio) assertEquals(ev.sql, "insert into foo values (?, ?)") - assertEquals(ev.args.allArgs, List(List("s", "k"))) + assertEquals(ev.params.allParams, List(List("s", "k"))) assertEquals(ev.label, "unlabeled") assert(ev.exec.toNanos > 0L) assert(ev.failure.getMessage.contains("Data conversion error")) @@ -176,7 +176,7 @@ class UpdateLogSuite extends munit.FunSuite { val ev = successEventForCIO(cio) assertEquals(ev.sql, "insert into foo values (?, ?)") - assertEquals(ev.args, NonBatch(List(1, "a"))) + assertEquals(ev.params, NonBatch(List(1, "a"))) assertEquals(ev.label, "unlabeled") assert(ev.exec.toNanos > 0L) assertEquals(ev.processing.toNanos, 0L) @@ -191,7 +191,7 @@ class UpdateLogSuite extends munit.FunSuite { val ev = execFailureEventForCIO(cio) assertEquals(ev.sql, "insert into foo") - assertEquals(ev.args, NonBatch(List(1, "a"))) + assertEquals(ev.params, NonBatch(List(1, "a"))) assertEquals(ev.label, "unlabeled") assertEquals(ev.exec.toNanos, 0L) assert(ev.failure.getMessage.contains("Syntax error")) @@ -205,7 +205,7 @@ class UpdateLogSuite extends munit.FunSuite { val ev = execFailureEventForCIO(cio) assertEquals(ev.sql, "insert into foo values (?, ?)") - assertEquals(ev.args, NonBatch(List("s", "k"))) + assertEquals(ev.params, NonBatch(List("s", "k"))) assertEquals(ev.label, "unlabeled") assert(ev.exec.toNanos > 0L) assert(ev.failure.getMessage.contains("Data conversion error")) @@ -218,7 +218,7 @@ class UpdateLogSuite extends munit.FunSuite { val ev = successEventForCIO(cio) assertEquals(ev.sql, "insert into foo values (?, ?)") - assertEquals(ev.args, NonBatch(List(1, "a"))) + assertEquals(ev.params, NonBatch(List(1, "a"))) assertEquals(ev.label, "unlabeled") assert(ev.exec.toNanos > 0L) assert(ev.processing.toNanos > 0L) @@ -231,7 +231,7 @@ class UpdateLogSuite extends munit.FunSuite { val ev = execFailureEventForCIO(cio) assertEquals(ev.sql, "insert into foo") - assertEquals(ev.args, NonBatch(List(1, "a"))) + assertEquals(ev.params, NonBatch(List(1, "a"))) assertEquals(ev.label, "unlabeled") assertEquals(ev.exec.toNanos, 0L) assert(ev.failure.getMessage.contains("Syntax error")) @@ -243,7 +243,7 @@ class UpdateLogSuite extends munit.FunSuite { val ev = execFailureEventForCIO(cio) assertEquals(ev.sql, "insert into foo values (?, ?)") - assertEquals(ev.args, NonBatch(List("s", "k"))) + assertEquals(ev.params, NonBatch(List("s", "k"))) assertEquals(ev.label, "unlabeled") assert(ev.exec.toNanos > 0L) assert(ev.failure.getMessage.contains("Data conversion error")) diff --git a/modules/docs/src/main/mdoc/docs/04-Selecting.md b/modules/docs/src/main/mdoc/docs/04-Selecting.md index 44512fca5..1f05b0eca 100644 --- a/modules/docs/src/main/mdoc/docs/04-Selecting.md +++ b/modules/docs/src/main/mdoc/docs/04-Selecting.md @@ -222,11 +222,21 @@ p.take(5).compile.toVector.unsafeRunSync().foreach(println) The `sql` interpolator is sugar for constructors defined in the `doobie.hi.connection` module, aliased as `HC` if you use the standard imports. Using these constructors directly, the above program would look like this: ```scala mdoc +import doobie.hi.HC +import doobie.free.{FC, FPS} +import doobie.util.log.{LoggingInfo, Parameters} +val sql = "select code, name, population, gnp from country" val proc = HC.stream[(Code, Country2)]( - "select code, name, population, gnp from country", // statement - ().pure[PreparedStatementIO], // prep (none) - 512 // chunk size + create = FC.prepareStatement(sql), // Create prepared statement + prep = FPS.unit, // prepare steps (none) + exec = FPS.executeQuery, // execute the query + chunkSize = 512, // chunk size + loggingInfo = LoggingInfo( + sql = sql, + params = Parameters.NonBatch(List.empty), // No parameters + label = "unlabeled" + ) ) proc.take(5) // Stream[ConnectionIO, (Code, Country2)] diff --git a/modules/docs/src/main/mdoc/docs/05-Parameterized.md b/modules/docs/src/main/mdoc/docs/05-Parameterized.md index 46821df1a..cc7190464 100644 --- a/modules/docs/src/main/mdoc/docs/05-Parameterized.md +++ b/modules/docs/src/main/mdoc/docs/05-Parameterized.md @@ -141,17 +141,33 @@ populationIn(100_000_000 to 300_000_000, NonEmptyList.of("USA", "BRA", "PAK", "G In the previous chapter's *Diving Deeper* we saw how a query constructed with the `sql` interpolator is just sugar for the `stream` constructor defined in the `doobie.hi.connection` module (aliased as `HC`). Here we see that the second parameter, a `PreparedStatementIO` program, is used to set the query parameters. The third parameter specifies a chunking factor; rows are buffered in chunks of the specified size. ```scala mdoc:silent +import doobie.hi.{HC, HPS} +import doobie.free.{FC, FPS} +import doobie.util.log.{LoggingInfo, Parameters} import fs2.Stream -val q = """ +val q = + """ select code, name, population, gnp from country where population > ? and population < ? """ -def proc(range: Range): Stream[ConnectionIO, Country] = - HC.stream[Country](q, HPS.set((range.min, range.max)), 512) +def proc(range: Range): Stream[ConnectionIO, Country] = { + val params = (range.min, range.max) + HC.stream[Country]( + create = FC.prepareStatement(q), + prep = HPS.set(params), + exec = FPS.executeQuery, + chunkSize = 512, + loggingInfo = LoggingInfo( + sql = q, + params = Parameters.NonBatch(Write[(Int, Int)].toList(params)), + label = "fetch_country_in_population_range" + ) + ) +} ``` Which produces the same output. diff --git a/modules/free/src/main/scala/doobie/util/log.scala b/modules/free/src/main/scala/doobie/util/log.scala index 68a3b079a..b77e5b925 100644 --- a/modules/free/src/main/scala/doobie/util/log.scala +++ b/modules/free/src/main/scala/doobie/util/log.scala @@ -15,26 +15,28 @@ import scala.concurrent.duration.FiniteDuration object log { // Wrapper for a few information about a query for logging purposes - final case class LoggingInfo(sql: String, args: Arguments, label: String) + final case class LoggingInfo(sql: String, params: Parameters, label: String) - sealed trait Arguments { - def allArgs: List[List[Any]] = { + /** Parameters used in a query. For queries using batch arguments (e.g. updateMany) argument list is constructed + * lazily. + */ + sealed trait Parameters { + def allParams: List[List[Any]] = { this match { - case arg: Arguments.NonBatch => List(arg.argsAsList) - case arg: Arguments.Batch => arg.argsAsLists() + case p: Parameters.NonBatch => List(p.paramsAsList) + case p: Parameters.Batch => p.paramsAsLists() } } } - object Arguments { - final case class NonBatch(argsAsList: List[Any]) extends Arguments - final case class Batch(argsAsLists: () => List[List[Any]]) extends Arguments + object Parameters { + final case class NonBatch(paramsAsList: List[Any]) extends Parameters + final case class Batch(paramsAsLists: () => List[List[Any]]) extends Parameters val nonBatchEmpty: NonBatch = NonBatch(Nil) } - /** Algebraic type of events that can be passed to a `LogHandler`, both parameterized by the argument type of the SQL - * input parameters (this is typically an `HList`). + /** Algebraic type of events that can be passed to a `LogHandler` * @group Events */ sealed abstract class LogEvent extends Product with Serializable { @@ -43,7 +45,7 @@ object log { def sql: String /** The query arguments. */ - def args: Arguments + def params: Parameters def label: String @@ -52,7 +54,7 @@ object log { /** @group Events */ final case class Success( sql: String, - args: Arguments, + params: Parameters, label: String, exec: FiniteDuration, processing: FiniteDuration @@ -61,7 +63,7 @@ object log { /** @group Events */ final case class ProcessingFailure( sql: String, - args: Arguments, + params: Parameters, label: String, exec: FiniteDuration, processing: FiniteDuration, @@ -69,7 +71,7 @@ object log { ) extends LogEvent /** @group Events */ - final case class ExecFailure(sql: String, args: Arguments, label: String, exec: FiniteDuration, failure: Throwable) + final case class ExecFailure(sql: String, params: Parameters, label: String, exec: FiniteDuration, failure: Throwable) extends LogEvent object LogEvent { @@ -79,7 +81,7 @@ object log { processDuration: FiniteDuration ): Success = Success( sql = info.sql, - args = info.args, + params = info.params, label = info.label, exec = execDuration, processing = processDuration @@ -93,7 +95,7 @@ object log { ): ProcessingFailure = ProcessingFailure( sql = info.sql, - args = info.args, + params = info.params, label = info.label, exec = execDuration, processing = processDuration, @@ -107,7 +109,7 @@ object log { ): ExecFailure = ExecFailure( sql = info.sql, - args = info.args, + params = info.params, label = info.label, exec = execDuration, failure = error @@ -144,8 +146,8 @@ object log { logEvent match { case Success(s, a, l, e1, e2) => val argsStr = a match { - case nonBatch: Arguments.NonBatch => s"[${nonBatch.argsAsList.mkString(", ")}]" - case _: Arguments.Batch => "" + case nonBatch: Parameters.NonBatch => s"[${nonBatch.paramsAsList.mkString(", ")}]" + case _: Parameters.Batch => "" } jdkLogger.info(s"""Successful Statement Execution: | @@ -158,7 +160,7 @@ object log { """.stripMargin) case ProcessingFailure(s, a, l, e1, e2, t) => - val argsStr = a.allArgs.map(thisArgs => thisArgs.mkString("(", ", ", ")")) + val argsStr = a.allParams.map(thisArgs => thisArgs.mkString("(", ", ", ")")) .mkString("[", ", ", "]") jdkLogger.severe(s"""Failed Resultset Processing: | @@ -172,7 +174,7 @@ object log { """.stripMargin) case ExecFailure(s, a, l, e1, t) => - val argsStr = a.allArgs.map(thisArgs => thisArgs.mkString("(", ", ", ")")) + val argsStr = a.allParams.map(thisArgs => thisArgs.mkString("(", ", ", ")")) .mkString("[", ", ", "]") jdkLogger.severe(s"""Failed Statement Execution: | diff --git a/modules/hikari/src/test/scala/doobie/ErrorHandlingSuite.scala b/modules/hikari/src/test/scala/doobie/ResourceCleanupSuite.scala similarity index 63% rename from modules/hikari/src/test/scala/doobie/ErrorHandlingSuite.scala rename to modules/hikari/src/test/scala/doobie/ResourceCleanupSuite.scala index 507385f8f..8f154715e 100644 --- a/modules/hikari/src/test/scala/doobie/ErrorHandlingSuite.scala +++ b/modules/hikari/src/test/scala/doobie/ResourceCleanupSuite.scala @@ -13,27 +13,26 @@ import cats.effect.std.Random import com.zaxxer.hikari.HikariConfig import doobie.hi.{connection => IHC, resultset => IHRS} import doobie.free.{connection => IFC, preparedstatement => IFPS, resultset => IFRS} -import doobie.util.log.{Arguments, LoggingInfo} +import doobie.util.log.{Parameters, LoggingInfo} import org.postgresql.util.PSQLException +import cats.effect.unsafe.implicits.global import scala.concurrent.duration._ -class ErrorHandlingSuite extends munit.FunSuite { +class ResourceCleanupSuite extends munit.FunSuite { - import cats.effect.unsafe.implicits.global - - val config = { - val c = new HikariConfig() - c.setDriverClassName("org.postgresql.Driver") - c.setJdbcUrl("jdbc:postgresql://localhost/postgres") - c.setUsername("postgres") - c.setPassword("password") - c.setMaximumPoolSize(4) - c - } - - test("query error handling") { + test("Connection is always closed when running a query (with or without errors)") { + val config = { + val c = new HikariConfig() + c.setDriverClassName("org.postgresql.Driver") + c.setJdbcUrl("jdbc:postgresql://localhost/postgres") + c.setUsername("postgres") + c.setPassword("password") + // Use a small pool so if there's a connection leak we'll find out easier + c.setMaximumPoolSize(2) + c + } val pool = HikariTransactor.fromHikariConfig[IO](config, logHandler = None) pool.use { xa => @@ -49,32 +48,31 @@ class ErrorHandlingSuite extends munit.FunSuite { case Right(_) => () // pass } } yield ()) - .replicateA_(100) + .parReplicateA_(100) .timeout(10.seconds) } yield () }.unsafeRunSync() - } def q(i: Int): ConnectionIO[NonEmptyList[Int]] = { i match { - case 0 => + case 0 => // Bad SQL provided when constructing PS IHC.executeWithResultSet( - IFC.prepareStatement("select * from not_exist"), - IFPS.unit, - IFPS.executeQuery, - IFRS.raiseError(new Exception("shouldn't reach here")), - loggingInfo + create = IFC.prepareStatement("select * from not_exist"), + prep = IFPS.unit, + exec = IFPS.executeQuery, + process = IFRS.raiseError(new Exception("shouldn't reach here")), + loggingInfo = loggingInfo ) - case 1 => + case 1 => // Error in PS "prepare" step IHC.executeWithResultSet( - IFC.prepareStatement("SELECT * FROM generate_series(1, ?)"), - IFPS.setInt(1, 10) *> IFPS.raiseError(new SimulatedError), - IFPS.executeQuery, - IFRS.raiseError(new Exception("shouldn't reach here")), - loggingInfo + create = IFC.prepareStatement("SELECT * FROM generate_series(1, ?)"), + prep = IFPS.setInt(1, 10) *> IFPS.raiseError(new SimulatedError), + exec = IFPS.raiseError(new Exception("shouldn't reach here")), + process = IFRS.raiseError(new Exception("shouldn't reach here")), + loggingInfo = loggingInfo ) - case 2 => + case 2 => // Error during execute step IHC.executeWithResultSet( IFC.prepareStatement("SELECT * FROM generate_series(1, ?)"), IFPS.setInt(1, 10), @@ -82,7 +80,7 @@ class ErrorHandlingSuite extends munit.FunSuite { IFRS.raiseError(new Exception("shouldn't reach here")), loggingInfo ) - case 3 => + case 3 => // Error during process step IHC.executeWithResultSet( IFC.prepareStatement("SELECT * FROM generate_series(1, ?)"), IFPS.setInt(1, 10), @@ -90,7 +88,7 @@ class ErrorHandlingSuite extends munit.FunSuite { IHRS.nel[Int] *> IFRS.raiseError(new SimulatedError), loggingInfo ) - case 4 => + case 4 => // No error IHC.executeWithResultSet( IFC.prepareStatement("SELECT * FROM generate_series(1, ?)"), IFPS.setInt(1, 10), @@ -104,7 +102,7 @@ class ErrorHandlingSuite extends munit.FunSuite { private val loggingInfo = LoggingInfo( "select * from not_exist", - Arguments.nonBatchEmpty, + Parameters.nonBatchEmpty, "unset" ) diff --git a/modules/log4cats/src/main/scala/doobie/log4cats/Log4CatsDebuggingLogHandler.scala b/modules/log4cats/src/main/scala/doobie/log4cats/Log4CatsDebuggingLogHandler.scala index 0ff0d95ed..6a7dcdc59 100644 --- a/modules/log4cats/src/main/scala/doobie/log4cats/Log4CatsDebuggingLogHandler.scala +++ b/modules/log4cats/src/main/scala/doobie/log4cats/Log4CatsDebuggingLogHandler.scala @@ -17,8 +17,8 @@ class Log4CatsDebuggingLogHandler[F[_]](logger: MessageLogger[F]) extends LogHan override def run(logEvent: LogEvent): F[Unit] = logEvent match { case Success(s, a, l, e1, e2) => { val argsStr = a match { - case nonBatch: Arguments.NonBatch => s"[${nonBatch.argsAsList.mkString(", ")}]" - case _: Arguments.Batch => "" + case nonBatch: Parameters.NonBatch => s"[${nonBatch.paramsAsList.mkString(", ")}]" + case _: Parameters.Batch => "" } logger.info( s"""Successful Statement Execution: @@ -33,7 +33,7 @@ class Log4CatsDebuggingLogHandler[F[_]](logger: MessageLogger[F]) extends LogHan } case ProcessingFailure(s, a, l, e1, e2, t) => { - val argsStr = a.allArgs.map(thisArgs => thisArgs.mkString("(", ", ", ")")) + val argsStr = a.allParams.map(thisArgs => thisArgs.mkString("(", ", ", ")")) .mkString("[", ", ", "]") logger.warn( s"""Failed Resultset Processing: @@ -49,7 +49,7 @@ class Log4CatsDebuggingLogHandler[F[_]](logger: MessageLogger[F]) extends LogHan } case ExecFailure(s, a, l, e1, t) => { - val argsStr = a.allArgs.map(thisArgs => thisArgs.mkString("(", ", ", ")")) + val argsStr = a.allParams.map(thisArgs => thisArgs.mkString("(", ", ", ")")) .mkString("[", ", ", "]") logger.error( s"""Failed Statement Execution: