From a3d1e277258eac1b8f26b6e7bcd5d41dc44b98fa Mon Sep 17 00:00:00 2001 From: Serhii Dashko Date: Mon, 30 May 2022 21:21:25 +0300 Subject: [PATCH 1/8] Adds HikariCP (#639) --- build.sbt | 20 +++- .../scala/zio/sql/HikariConnectionPool.scala | 34 ++++++ .../zio/sql/HikariConnectionPoolConfig.scala | 53 +++++++++ .../zio/sql/HikariConnectionPoolSpec.scala | 102 ++++++++++++++++++ .../scala/zio/sql/MySqlTestContainer.scala | 20 ++++ 5 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 jdbc-hikaricp/src/main/scala/zio/sql/HikariConnectionPool.scala create mode 100644 jdbc-hikaricp/src/main/scala/zio/sql/HikariConnectionPoolConfig.scala create mode 100644 jdbc-hikaricp/src/test/scala/zio/sql/HikariConnectionPoolSpec.scala create mode 100644 jdbc-hikaricp/src/test/scala/zio/sql/MySqlTestContainer.scala diff --git a/build.sbt b/build.sbt index a321d57f0..a185948df 100644 --- a/build.sbt +++ b/build.sbt @@ -69,7 +69,8 @@ lazy val root = project mysql, oracle, postgres, - sqlserver + sqlserver, + jdbc_hikaricp ) lazy val core = crossProject(JSPlatform, JVMPlatform) @@ -155,6 +156,23 @@ lazy val jdbc = project .settings(testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework")) .dependsOn(core.jvm) +lazy val jdbc_hikaricp = project + .in(file("jdbc-hikaricp")) + .settings(stdSettings("zio-sql-jdbc-hickaricp")) + .settings(buildInfoSettings("zio.sql.jdbc-hickaricp")) + .settings( + libraryDependencies ++= Seq( + "com.zaxxer" % "HikariCP" % "5.0.1", + "dev.zio" %% "zio-test" % zioVersion % Test, + "dev.zio" %% "zio-test-sbt" % zioVersion % Test, + "org.testcontainers" % "mysql" % testcontainersVersion % Test, + "mysql" % "mysql-connector-java" % "8.0.29" % Test, + "com.dimafeng" %% "testcontainers-scala-mysql" % testcontainersScalaVersion % Test + ) + ) + .settings(testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework")) + .dependsOn(jdbc) + lazy val mysql = project .in(file("mysql")) .dependsOn(jdbc % "compile->compile;test->test") diff --git a/jdbc-hikaricp/src/main/scala/zio/sql/HikariConnectionPool.scala b/jdbc-hikaricp/src/main/scala/zio/sql/HikariConnectionPool.scala new file mode 100644 index 000000000..8696eb1f9 --- /dev/null +++ b/jdbc-hikaricp/src/main/scala/zio/sql/HikariConnectionPool.scala @@ -0,0 +1,34 @@ +package zio.sql +import com.zaxxer.hikari.{HikariConfig, HikariDataSource} +import zio.{Scope, ZIO, ZLayer} + +import java.sql.{Connection, SQLException} + +class HikariConnectionPool private (hikariDataSource: HikariDataSource) extends ConnectionPool { + + private[sql] val dataSource = hikariDataSource + + /** + * Retrieves a JDBC java.sql.Connection as a [[ZIO[Scope, Exception, Connection]]] resource. + * The managed resource will safely acquire and release the connection, and + * may be interrupted or timed out if necessary. + */ + override def connection: ZIO[Scope, Exception, Connection] = { + ZIO.acquireRelease(ZIO.attemptBlocking(hikariDataSource.getConnection).refineToOrDie[SQLException])(con => ZIO.attemptBlocking(hikariDataSource.evictConnection(con)).orDie) + } +} + +object HikariConnectionPool { + + private[sql] def initDataSource(config: HikariConfig): ZIO[Scope, Throwable, HikariDataSource] = + ZIO.acquireRelease(ZIO.attemptBlocking(new HikariDataSource(config)))(ds => ZIO.attemptBlocking(ds.close()).orDie) + + val live: ZLayer[HikariConnectionPoolConfig, Throwable, HikariConnectionPool] = + ZLayer.scoped { + for { + config <- ZIO.service[HikariConnectionPoolConfig] + dataSource <- initDataSource(config.toHikariConfig) + pool = new HikariConnectionPool(dataSource) + } yield pool + } +} diff --git a/jdbc-hikaricp/src/main/scala/zio/sql/HikariConnectionPoolConfig.scala b/jdbc-hikaricp/src/main/scala/zio/sql/HikariConnectionPoolConfig.scala new file mode 100644 index 000000000..f788a9028 --- /dev/null +++ b/jdbc-hikaricp/src/main/scala/zio/sql/HikariConnectionPoolConfig.scala @@ -0,0 +1,53 @@ +package zio.sql + +import com.zaxxer.hikari.HikariConfig + + +/** + * Configuration information for the connection pool. + * + * @param url The JDBC connection string. + * @param properties JDBC connection properties (username / password could go here). + * @param poolSize The size of the pool. + * @param connectionTimeout Maximum number of milliseconds that a client will wait for a connection from the pool. + * If this time is exceeded without a connection becoming available, a SQLException will be thrown from javax.sql.DataSource.getConnection(). + * @param idleTimeout This property controls the maximum amount of time (in milliseconds) that a connection is allowed to sit idle in the pool. + * Whether a connection is retired as idle or not is subject to a maximum variation of +30 seconds, and average variation of +15 seconds. + * A connection will never be retired as idle before this timeout. A value of 0 means that idle connections are never removed from the pool. + * @param initializationFailTimeout the number of milliseconds before the + * pool initialization fails, or 0 to validate connection setup but continue with + * pool start, or less than zero to skip all initialization checks and start the + * pool without delay. + * @param maxLifetime This property controls the maximum lifetime of a connection in the pool. + * When a connection reaches this timeout, even if recently used, it will be retired from the pool. + * An in-use connection will never be retired, only when it is idle will it be removed. Should be bigger then 30000 + * @param minimumIdle The property controls the minimum number of idle connections that HikariCP tries to maintain in the pool, including both idle and in-use connections. + * If the idle connections dip below this value, HikariCP will make a best effort to restore them quickly and efficiently. + */ +final case class HikariConnectionPoolConfig( + url: String, + userName: String, + password: String, + poolSize: Int = 10, + autoCommit: Boolean = true, + connectionTimeout: Option[Long] = None, + idleTimeout: Option[Long] = None, + initializationFailTimeout: Option[Long] = None, + maxLifetime: Option[Long] = None, + minimumIdle: Option[Int] = None + ) { + private[sql] def toHikariConfig = { + val hikariConfig = new HikariConfig() + hikariConfig.setJdbcUrl(this.url) + hikariConfig.setAutoCommit(this.autoCommit) + hikariConfig.setMaximumPoolSize(this.poolSize) + hikariConfig.setUsername(userName) + hikariConfig.setPassword(password) + connectionTimeout.foreach(hikariConfig.setConnectionTimeout) + idleTimeout.foreach(hikariConfig.setIdleTimeout) + initializationFailTimeout.foreach(hikariConfig.setInitializationFailTimeout) + maxLifetime.foreach(hikariConfig.setMaxLifetime) + minimumIdle.foreach(hikariConfig.setMinimumIdle) + hikariConfig + } +} diff --git a/jdbc-hikaricp/src/test/scala/zio/sql/HikariConnectionPoolSpec.scala b/jdbc-hikaricp/src/test/scala/zio/sql/HikariConnectionPoolSpec.scala new file mode 100644 index 000000000..8f3d40b3e --- /dev/null +++ b/jdbc-hikaricp/src/test/scala/zio/sql/HikariConnectionPoolSpec.scala @@ -0,0 +1,102 @@ +package zio.sql + +import zio.test.TestAspect.{sequential, timeout, withLiveClock} +import zio.test.{TestEnvironment, _} +import zio.{ZIO, ZLayer, durationInt} + + +object HikariConnectionPoolSpec extends ZIOSpecDefault { + + + val mySqlConfigLayer: ZLayer[Any, Throwable, MySqlConfig] = + ZLayer.scoped { + MySqlTestContainer.mysql() + .map(a => + MySqlConfig( + url = a.jdbcUrl, + username = a.username, + password = a.password + ) + ) + } + + val hikariPoolConfigLayer: ZLayer[MySqlConfig, Nothing, HikariConnectionPoolConfig] = ZLayer.fromFunction((conf: MySqlConfig) => HikariConnectionPoolConfig(url = conf.url, userName = conf.username, password = conf.password)) + val poolLayer: ZLayer[HikariConnectionPoolConfig, Nothing, HikariConnectionPool] = HikariConnectionPool.live.orDie + + override def spec: Spec[TestEnvironment, Any] = + specLayered.provideCustomShared(mySqlConfigLayer.orDie) + + def specLayered: Spec[TestEnvironment with MySqlConfig, Any] = + suite("Hikaricp module")( + test("Pool size should be configurable") { + val poolSize = 20 + (for { + cp <- ZIO.service[HikariConnectionPool] + } yield assertTrue(cp.dataSource.getMaximumPoolSize == poolSize)).provideSomeLayer[TestEnvironment with MySqlConfig](hikariPoolConfigLayer.map(_.update(_.copy(poolSize = poolSize))) >>> poolLayer) + } @@ timeout(10.seconds) @@ withLiveClock, + + test("Pool size should have 10 connections by default") { + (for { + cp <- ZIO.service[HikariConnectionPool] + _ <- ZIO.replicateZIO(10)(ZIO.scoped(cp.connection)) + } yield assertTrue(cp.dataSource.getMaximumPoolSize == 10)).provideSomeLayer[TestEnvironment with MySqlConfig](hikariPoolConfigLayer >>> poolLayer) + } @@ timeout(10.minutes) @@ withLiveClock, + + test("It should be possible to acquire connections from the pool") { + val poolSize = 20 + (for { + cp <- ZIO.service[HikariConnectionPool] + _ <- ZIO.collectAllParDiscard(ZIO.replicate(poolSize)(ZIO.scoped(cp.connection *> ZIO.sleep(500.millisecond)))) + } yield assert("")(Assertion.anything)).provideSomeLayer[TestEnvironment with MySqlConfig](hikariPoolConfigLayer.map(_.update(_.copy(poolSize = poolSize))) >>> poolLayer) + } @@ timeout(10.seconds) @@ withLiveClock, + + test("Auto commit should be configurable") { + val autoCommit = false + (for { + cp <- ZIO.service[HikariConnectionPool] + } yield assertTrue(cp.dataSource.isAutoCommit == autoCommit)).provideSomeLayer[TestEnvironment with MySqlConfig](hikariPoolConfigLayer.map(_.update(_.copy(autoCommit = autoCommit))) >>> poolLayer) + } @@ timeout(10.seconds) @@ withLiveClock, + + test("Auto commit should be true by default") { + (for { + cp <- ZIO.service[HikariConnectionPool] + } yield assertTrue(cp.dataSource.isAutoCommit)).provideSomeLayer[TestEnvironment with MySqlConfig](hikariPoolConfigLayer >>> poolLayer) + } @@ timeout(10.seconds) @@ withLiveClock, + + test("Connection timeout should be configurable") { + val connectionTimeout = 2000L + (for { + cp <- ZIO.service[HikariConnectionPool] + } yield assertTrue(cp.dataSource.getConnectionTimeout == connectionTimeout)).provideSomeLayer[TestEnvironment with MySqlConfig](hikariPoolConfigLayer.map(_.update(_.copy(connectionTimeout = Some(connectionTimeout)))) >>> poolLayer) + } @@ timeout(10.seconds) @@ withLiveClock, + + test("Idle timeout should be configurable") { + val idleTimeout = 2000L + (for { + cp <- ZIO.service[HikariConnectionPool] + } yield assertTrue(cp.dataSource.getIdleTimeout == idleTimeout)).provideSomeLayer[TestEnvironment with MySqlConfig](hikariPoolConfigLayer.map(_.update(_.copy(idleTimeout = Some(idleTimeout)))) >>> poolLayer) + } @@ timeout(10.seconds) @@ withLiveClock, + + test("initialization fail timeout should be configurable") { + val initializationFailTimeout = 2000L + (for { + cp <- ZIO.service[HikariConnectionPool] + } yield assertTrue(cp.dataSource.getInitializationFailTimeout == initializationFailTimeout)).provideSomeLayer[TestEnvironment with MySqlConfig](hikariPoolConfigLayer.map(_.update(_.copy(initializationFailTimeout = Some(initializationFailTimeout)))) >>> poolLayer) + } @@ timeout(10.seconds) @@ withLiveClock, + + test("max lifetime should be configurable") { + val maxLifetime = 40000L + (for { + cp <- ZIO.service[HikariConnectionPool] + } yield assertTrue(cp.dataSource.getMaxLifetime == maxLifetime)).provideSomeLayer[TestEnvironment with MySqlConfig](hikariPoolConfigLayer.map(_.update(_.copy(maxLifetime = Some(maxLifetime)))) >>> poolLayer) + } @@ timeout(10.seconds) @@ withLiveClock, + + test("minimum idle should be configurable") { + val minimumIdle = 2 + (for { + cp <- ZIO.service[HikariConnectionPool] + } yield assertTrue(cp.dataSource.getMinimumIdle == minimumIdle)).provideSomeLayer[TestEnvironment with MySqlConfig](hikariPoolConfigLayer.map(_.update(_.copy(minimumIdle = Some(minimumIdle)))) >>> poolLayer) + } @@ timeout(10.seconds) @@ withLiveClock, + + ) @@ sequential +} diff --git a/jdbc-hikaricp/src/test/scala/zio/sql/MySqlTestContainer.scala b/jdbc-hikaricp/src/test/scala/zio/sql/MySqlTestContainer.scala new file mode 100644 index 000000000..ebba6fa31 --- /dev/null +++ b/jdbc-hikaricp/src/test/scala/zio/sql/MySqlTestContainer.scala @@ -0,0 +1,20 @@ +package zio.sql + +import com.dimafeng.testcontainers.MySQLContainer +import org.testcontainers.utility.DockerImageName +import zio._ + +final case class MySqlConfig(username: String, password: String, url: String) +object MySqlTestContainer { + + def mysql(imageName: String = "mysql"): ZIO[Scope, Throwable, MySQLContainer] = + ZIO.acquireRelease { + ZIO.attemptBlocking { + val c = new MySQLContainer( + mysqlImageVersion = Option(imageName).map(DockerImageName.parse) + ) + c.start() + c + } + }(container => ZIO.attemptBlocking(container.stop()).orDie) +} From 9658c654f1da7a0db8d15640d9d98b9efb336e14 Mon Sep 17 00:00:00 2001 From: Serhii Dashko Date: Tue, 31 May 2022 12:24:56 +0300 Subject: [PATCH 2/8] Adds option for connectionInitSql (#639) --- .../scala/zio/sql/HikariConnectionPool.scala | 17 ++-- .../zio/sql/HikariConnectionPoolConfig.scala | 29 +++--- .../zio/sql/HikariConnectionPoolSpec.scala | 89 ++++++++++++------- .../scala/zio/sql/MySqlTestContainer.scala | 5 +- 4 files changed, 89 insertions(+), 51 deletions(-) diff --git a/jdbc-hikaricp/src/main/scala/zio/sql/HikariConnectionPool.scala b/jdbc-hikaricp/src/main/scala/zio/sql/HikariConnectionPool.scala index 8696eb1f9..b72a28464 100644 --- a/jdbc-hikaricp/src/main/scala/zio/sql/HikariConnectionPool.scala +++ b/jdbc-hikaricp/src/main/scala/zio/sql/HikariConnectionPool.scala @@ -1,8 +1,8 @@ package zio.sql -import com.zaxxer.hikari.{HikariConfig, HikariDataSource} -import zio.{Scope, ZIO, ZLayer} +import com.zaxxer.hikari.{ HikariConfig, HikariDataSource } +import zio.{ Scope, ZIO, ZLayer } -import java.sql.{Connection, SQLException} +import java.sql.{ Connection, SQLException } class HikariConnectionPool private (hikariDataSource: HikariDataSource) extends ConnectionPool { @@ -13,9 +13,10 @@ class HikariConnectionPool private (hikariDataSource: HikariDataSource) extends * The managed resource will safely acquire and release the connection, and * may be interrupted or timed out if necessary. */ - override def connection: ZIO[Scope, Exception, Connection] = { - ZIO.acquireRelease(ZIO.attemptBlocking(hikariDataSource.getConnection).refineToOrDie[SQLException])(con => ZIO.attemptBlocking(hikariDataSource.evictConnection(con)).orDie) - } + override def connection: ZIO[Scope, Exception, Connection] = + ZIO.acquireRelease(ZIO.attemptBlocking(hikariDataSource.getConnection).refineToOrDie[SQLException])(con => + ZIO.attemptBlocking(hikariDataSource.evictConnection(con)).orDie + ) } object HikariConnectionPool { @@ -26,9 +27,9 @@ object HikariConnectionPool { val live: ZLayer[HikariConnectionPoolConfig, Throwable, HikariConnectionPool] = ZLayer.scoped { for { - config <- ZIO.service[HikariConnectionPoolConfig] + config <- ZIO.service[HikariConnectionPoolConfig] dataSource <- initDataSource(config.toHikariConfig) - pool = new HikariConnectionPool(dataSource) + pool = new HikariConnectionPool(dataSource) } yield pool } } diff --git a/jdbc-hikaricp/src/main/scala/zio/sql/HikariConnectionPoolConfig.scala b/jdbc-hikaricp/src/main/scala/zio/sql/HikariConnectionPoolConfig.scala index f788a9028..45ab36908 100644 --- a/jdbc-hikaricp/src/main/scala/zio/sql/HikariConnectionPoolConfig.scala +++ b/jdbc-hikaricp/src/main/scala/zio/sql/HikariConnectionPoolConfig.scala @@ -2,7 +2,6 @@ package zio.sql import com.zaxxer.hikari.HikariConfig - /** * Configuration information for the connection pool. * @@ -23,19 +22,24 @@ import com.zaxxer.hikari.HikariConfig * An in-use connection will never be retired, only when it is idle will it be removed. Should be bigger then 30000 * @param minimumIdle The property controls the minimum number of idle connections that HikariCP tries to maintain in the pool, including both idle and in-use connections. * If the idle connections dip below this value, HikariCP will make a best effort to restore them quickly and efficiently. + * @param connectionInitSql the SQL to execute on new connections + * Set the SQL string that will be executed on all new connections when they are + * created, before they are added to the pool. If this query fails, it will be + * treated as a failed connection attempt. */ final case class HikariConnectionPoolConfig( - url: String, - userName: String, - password: String, - poolSize: Int = 10, - autoCommit: Boolean = true, - connectionTimeout: Option[Long] = None, - idleTimeout: Option[Long] = None, - initializationFailTimeout: Option[Long] = None, - maxLifetime: Option[Long] = None, - minimumIdle: Option[Int] = None - ) { + url: String, + userName: String, + password: String, + poolSize: Int = 10, + autoCommit: Boolean = true, + connectionTimeout: Option[Long] = None, + idleTimeout: Option[Long] = None, + initializationFailTimeout: Option[Long] = None, + maxLifetime: Option[Long] = None, + minimumIdle: Option[Int] = None, + connectionInitSql: Option[String] = None +) { private[sql] def toHikariConfig = { val hikariConfig = new HikariConfig() hikariConfig.setJdbcUrl(this.url) @@ -48,6 +52,7 @@ final case class HikariConnectionPoolConfig( initializationFailTimeout.foreach(hikariConfig.setInitializationFailTimeout) maxLifetime.foreach(hikariConfig.setMaxLifetime) minimumIdle.foreach(hikariConfig.setMinimumIdle) + connectionInitSql.foreach(hikariConfig.setConnectionInitSql) hikariConfig } } diff --git a/jdbc-hikaricp/src/test/scala/zio/sql/HikariConnectionPoolSpec.scala b/jdbc-hikaricp/src/test/scala/zio/sql/HikariConnectionPoolSpec.scala index 8f3d40b3e..31a2a5f5b 100644 --- a/jdbc-hikaricp/src/test/scala/zio/sql/HikariConnectionPoolSpec.scala +++ b/jdbc-hikaricp/src/test/scala/zio/sql/HikariConnectionPoolSpec.scala @@ -1,16 +1,15 @@ package zio.sql -import zio.test.TestAspect.{sequential, timeout, withLiveClock} -import zio.test.{TestEnvironment, _} -import zio.{ZIO, ZLayer, durationInt} - +import zio.test.TestAspect.{ sequential, timeout, withLiveClock } +import zio.test.{ TestEnvironment, _ } +import zio.{ durationInt, ZIO, ZLayer } object HikariConnectionPoolSpec extends ZIOSpecDefault { - val mySqlConfigLayer: ZLayer[Any, Throwable, MySqlConfig] = ZLayer.scoped { - MySqlTestContainer.mysql() + MySqlTestContainer + .mysql() .map(a => MySqlConfig( url = a.jdbcUrl, @@ -20,8 +19,11 @@ object HikariConnectionPoolSpec extends ZIOSpecDefault { ) } - val hikariPoolConfigLayer: ZLayer[MySqlConfig, Nothing, HikariConnectionPoolConfig] = ZLayer.fromFunction((conf: MySqlConfig) => HikariConnectionPoolConfig(url = conf.url, userName = conf.username, password = conf.password)) - val poolLayer: ZLayer[HikariConnectionPoolConfig, Nothing, HikariConnectionPool] = HikariConnectionPool.live.orDie + val hikariPoolConfigLayer: ZLayer[MySqlConfig, Nothing, HikariConnectionPoolConfig] = + ZLayer.fromFunction((conf: MySqlConfig) => + HikariConnectionPoolConfig(url = conf.url, userName = conf.username, password = conf.password) + ) + val poolLayer: ZLayer[HikariConnectionPoolConfig, Nothing, HikariConnectionPool] = HikariConnectionPool.live.orDie override def spec: Spec[TestEnvironment, Any] = specLayered.provideCustomShared(mySqlConfigLayer.orDie) @@ -29,74 +31,101 @@ object HikariConnectionPoolSpec extends ZIOSpecDefault { def specLayered: Spec[TestEnvironment with MySqlConfig, Any] = suite("Hikaricp module")( test("Pool size should be configurable") { - val poolSize = 20 + val poolSize = 20 (for { cp <- ZIO.service[HikariConnectionPool] - } yield assertTrue(cp.dataSource.getMaximumPoolSize == poolSize)).provideSomeLayer[TestEnvironment with MySqlConfig](hikariPoolConfigLayer.map(_.update(_.copy(poolSize = poolSize))) >>> poolLayer) + } yield assertTrue(cp.dataSource.getMaximumPoolSize == poolSize)) + .provideSomeLayer[TestEnvironment with MySqlConfig]( + hikariPoolConfigLayer.map(_.update(_.copy(poolSize = poolSize))) >>> poolLayer + ) } @@ timeout(10.seconds) @@ withLiveClock, - test("Pool size should have 10 connections by default") { (for { cp <- ZIO.service[HikariConnectionPool] _ <- ZIO.replicateZIO(10)(ZIO.scoped(cp.connection)) - } yield assertTrue(cp.dataSource.getMaximumPoolSize == 10)).provideSomeLayer[TestEnvironment with MySqlConfig](hikariPoolConfigLayer >>> poolLayer) + } yield assertTrue(cp.dataSource.getMaximumPoolSize == 10)) + .provideSomeLayer[TestEnvironment with MySqlConfig](hikariPoolConfigLayer >>> poolLayer) } @@ timeout(10.minutes) @@ withLiveClock, - test("It should be possible to acquire connections from the pool") { val poolSize = 20 (for { cp <- ZIO.service[HikariConnectionPool] - _ <- ZIO.collectAllParDiscard(ZIO.replicate(poolSize)(ZIO.scoped(cp.connection *> ZIO.sleep(500.millisecond)))) - } yield assert("")(Assertion.anything)).provideSomeLayer[TestEnvironment with MySqlConfig](hikariPoolConfigLayer.map(_.update(_.copy(poolSize = poolSize))) >>> poolLayer) + _ <- + ZIO.collectAllParDiscard(ZIO.replicate(poolSize)(ZIO.scoped(cp.connection *> ZIO.sleep(500.millisecond)))) + } yield assert("")(Assertion.anything)).provideSomeLayer[TestEnvironment with MySqlConfig]( + hikariPoolConfigLayer.map(_.update(_.copy(poolSize = poolSize))) >>> poolLayer + ) } @@ timeout(10.seconds) @@ withLiveClock, - test("Auto commit should be configurable") { val autoCommit = false (for { cp <- ZIO.service[HikariConnectionPool] - } yield assertTrue(cp.dataSource.isAutoCommit == autoCommit)).provideSomeLayer[TestEnvironment with MySqlConfig](hikariPoolConfigLayer.map(_.update(_.copy(autoCommit = autoCommit))) >>> poolLayer) + } yield assertTrue(cp.dataSource.isAutoCommit == autoCommit)) + .provideSomeLayer[TestEnvironment with MySqlConfig]( + hikariPoolConfigLayer.map(_.update(_.copy(autoCommit = autoCommit))) >>> poolLayer + ) } @@ timeout(10.seconds) @@ withLiveClock, - test("Auto commit should be true by default") { (for { cp <- ZIO.service[HikariConnectionPool] - } yield assertTrue(cp.dataSource.isAutoCommit)).provideSomeLayer[TestEnvironment with MySqlConfig](hikariPoolConfigLayer >>> poolLayer) + } yield assertTrue(cp.dataSource.isAutoCommit)) + .provideSomeLayer[TestEnvironment with MySqlConfig](hikariPoolConfigLayer >>> poolLayer) } @@ timeout(10.seconds) @@ withLiveClock, - test("Connection timeout should be configurable") { val connectionTimeout = 2000L (for { cp <- ZIO.service[HikariConnectionPool] - } yield assertTrue(cp.dataSource.getConnectionTimeout == connectionTimeout)).provideSomeLayer[TestEnvironment with MySqlConfig](hikariPoolConfigLayer.map(_.update(_.copy(connectionTimeout = Some(connectionTimeout)))) >>> poolLayer) + } yield assertTrue(cp.dataSource.getConnectionTimeout == connectionTimeout)) + .provideSomeLayer[TestEnvironment with MySqlConfig]( + hikariPoolConfigLayer.map(_.update(_.copy(connectionTimeout = Some(connectionTimeout)))) >>> poolLayer + ) } @@ timeout(10.seconds) @@ withLiveClock, - test("Idle timeout should be configurable") { val idleTimeout = 2000L (for { cp <- ZIO.service[HikariConnectionPool] - } yield assertTrue(cp.dataSource.getIdleTimeout == idleTimeout)).provideSomeLayer[TestEnvironment with MySqlConfig](hikariPoolConfigLayer.map(_.update(_.copy(idleTimeout = Some(idleTimeout)))) >>> poolLayer) + } yield assertTrue(cp.dataSource.getIdleTimeout == idleTimeout)) + .provideSomeLayer[TestEnvironment with MySqlConfig]( + hikariPoolConfigLayer.map(_.update(_.copy(idleTimeout = Some(idleTimeout)))) >>> poolLayer + ) } @@ timeout(10.seconds) @@ withLiveClock, - test("initialization fail timeout should be configurable") { val initializationFailTimeout = 2000L (for { cp <- ZIO.service[HikariConnectionPool] - } yield assertTrue(cp.dataSource.getInitializationFailTimeout == initializationFailTimeout)).provideSomeLayer[TestEnvironment with MySqlConfig](hikariPoolConfigLayer.map(_.update(_.copy(initializationFailTimeout = Some(initializationFailTimeout)))) >>> poolLayer) + } yield assertTrue(cp.dataSource.getInitializationFailTimeout == initializationFailTimeout)) + .provideSomeLayer[TestEnvironment with MySqlConfig]( + hikariPoolConfigLayer.map( + _.update(_.copy(initializationFailTimeout = Some(initializationFailTimeout))) + ) >>> poolLayer + ) } @@ timeout(10.seconds) @@ withLiveClock, - test("max lifetime should be configurable") { val maxLifetime = 40000L (for { cp <- ZIO.service[HikariConnectionPool] - } yield assertTrue(cp.dataSource.getMaxLifetime == maxLifetime)).provideSomeLayer[TestEnvironment with MySqlConfig](hikariPoolConfigLayer.map(_.update(_.copy(maxLifetime = Some(maxLifetime)))) >>> poolLayer) + } yield assertTrue(cp.dataSource.getMaxLifetime == maxLifetime)) + .provideSomeLayer[TestEnvironment with MySqlConfig]( + hikariPoolConfigLayer.map(_.update(_.copy(maxLifetime = Some(maxLifetime)))) >>> poolLayer + ) } @@ timeout(10.seconds) @@ withLiveClock, - test("minimum idle should be configurable") { val minimumIdle = 2 (for { cp <- ZIO.service[HikariConnectionPool] - } yield assertTrue(cp.dataSource.getMinimumIdle == minimumIdle)).provideSomeLayer[TestEnvironment with MySqlConfig](hikariPoolConfigLayer.map(_.update(_.copy(minimumIdle = Some(minimumIdle)))) >>> poolLayer) + } yield assertTrue(cp.dataSource.getMinimumIdle == minimumIdle)) + .provideSomeLayer[TestEnvironment with MySqlConfig]( + hikariPoolConfigLayer.map(_.update(_.copy(minimumIdle = Some(minimumIdle)))) >>> poolLayer + ) } @@ timeout(10.seconds) @@ withLiveClock, - + test("connection init SQL should be configurable") { + val initialSql = "SELECT 1 FROM test.test" + (for { + cp <- ZIO.service[HikariConnectionPool] + } yield assertTrue(cp.dataSource.getConnectionInitSql == initialSql)) + .provideSomeLayer[TestEnvironment with MySqlConfig]( + hikariPoolConfigLayer.map(_.update(_.copy(connectionInitSql = Some(initialSql)))) >>> poolLayer + ) + } @@ timeout(10.seconds) @@ withLiveClock ) @@ sequential } diff --git a/jdbc-hikaricp/src/test/scala/zio/sql/MySqlTestContainer.scala b/jdbc-hikaricp/src/test/scala/zio/sql/MySqlTestContainer.scala index ebba6fa31..16ec7cc17 100644 --- a/jdbc-hikaricp/src/test/scala/zio/sql/MySqlTestContainer.scala +++ b/jdbc-hikaricp/src/test/scala/zio/sql/MySqlTestContainer.scala @@ -12,7 +12,10 @@ object MySqlTestContainer { ZIO.attemptBlocking { val c = new MySQLContainer( mysqlImageVersion = Option(imageName).map(DockerImageName.parse) - ) + ).configure { a => + a.withInitScript("test_schema.sql") + () + } c.start() c } From b3e3468da08e6c664db6482856f130eadd5686be Mon Sep 17 00:00:00 2001 From: Scala Steward Date: Thu, 18 Aug 2022 19:51:47 +0000 Subject: [PATCH 3/8] Update postgresql to 42.4.2 --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 2b81d0e3b..079abeb63 100644 --- a/build.sbt +++ b/build.sbt @@ -117,7 +117,7 @@ lazy val jdbc = project libraryDependencies ++= Seq( "dev.zio" %% "zio-test" % zioVersion % Test, "dev.zio" %% "zio-test-sbt" % zioVersion % Test, - "org.postgresql" % "postgresql" % "42.4.1" % Test, + "org.postgresql" % "postgresql" % "42.4.2" % Test, "com.dimafeng" %% "testcontainers-scala-postgresql" % testcontainersScalaVersion % Test ) ) @@ -181,7 +181,7 @@ lazy val postgres = project "org.testcontainers" % "database-commons" % testcontainersVersion % Test, "org.testcontainers" % "postgresql" % testcontainersVersion % Test, "org.testcontainers" % "jdbc" % testcontainersVersion % Test, - "org.postgresql" % "postgresql" % "42.4.1" % Compile, + "org.postgresql" % "postgresql" % "42.4.2" % Compile, "com.dimafeng" %% "testcontainers-scala-postgresql" % testcontainersScalaVersion % Test, "ch.qos.logback" % "logback-classic" % logbackVersion % Test ) From 15594b91f84a201e95fa875735864bd6f43f031b Mon Sep 17 00:00:00 2001 From: Scala Steward Date: Fri, 26 Aug 2022 18:40:09 +0000 Subject: [PATCH 4/8] Update ojdbc8 to 21.7.0.0 --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 2b81d0e3b..b0ee2d7e2 100644 --- a/build.sbt +++ b/build.sbt @@ -162,7 +162,7 @@ lazy val oracle = project "org.testcontainers" % "database-commons" % testcontainersVersion % Test, "org.testcontainers" % "oracle-xe" % testcontainersVersion % Test, "org.testcontainers" % "jdbc" % testcontainersVersion % Test, - "com.oracle.database.jdbc" % "ojdbc8" % "21.6.0.0.1" % Test, + "com.oracle.database.jdbc" % "ojdbc8" % "21.7.0.0" % Test, "com.dimafeng" %% "testcontainers-scala-oracle-xe" % testcontainersScalaVersion % Test, "ch.qos.logback" % "logback-classic" % logbackVersion % Test ) From ece5d0db3501494b8b8a431e2c6e93f3681523b4 Mon Sep 17 00:00:00 2001 From: jczuchnowski Date: Wed, 28 Sep 2022 18:12:42 +0200 Subject: [PATCH 5/8] Update zio to 2.0.2 --- build.sbt | 2 +- .../src/main/scala/zio/sql/HikariConnectionPool.scala | 5 ----- .../src/test/scala/zio/sql/HikariConnectionPoolSpec.scala | 2 +- .../src/test/scala/zio/sql/MySqlTestContainer.scala | 5 +---- jdbc/src/test/scala/zio/sql/JdbcRunnableSpec.scala | 7 ++++--- .../test/scala/zio/sql/oracle/OracleSqlModuleSpec.scala | 5 +++-- .../test/scala/zio/sql/sqlserver/SqlServerModuleSpec.scala | 2 +- 7 files changed, 11 insertions(+), 17 deletions(-) diff --git a/build.sbt b/build.sbt index cb9a09b02..5941858bb 100644 --- a/build.sbt +++ b/build.sbt @@ -23,7 +23,7 @@ addCommandAlias("fmtOnce", "all scalafmtSbt scalafmt test:scalafmt") addCommandAlias("fmt", "fmtOnce;fmtOnce") addCommandAlias("check", "all scalafmtSbtCheck scalafmtCheck test:scalafmtCheck") -val zioVersion = "2.0.0" +val zioVersion = "2.0.2" val zioSchemaVersion = "0.2.0" val testcontainersVersion = "1.17.3" val testcontainersScalaVersion = "0.40.10" diff --git a/jdbc-hikaricp/src/main/scala/zio/sql/HikariConnectionPool.scala b/jdbc-hikaricp/src/main/scala/zio/sql/HikariConnectionPool.scala index b72a28464..fa1eadec1 100644 --- a/jdbc-hikaricp/src/main/scala/zio/sql/HikariConnectionPool.scala +++ b/jdbc-hikaricp/src/main/scala/zio/sql/HikariConnectionPool.scala @@ -8,11 +8,6 @@ class HikariConnectionPool private (hikariDataSource: HikariDataSource) extends private[sql] val dataSource = hikariDataSource - /** - * Retrieves a JDBC java.sql.Connection as a [[ZIO[Scope, Exception, Connection]]] resource. - * The managed resource will safely acquire and release the connection, and - * may be interrupted or timed out if necessary. - */ override def connection: ZIO[Scope, Exception, Connection] = ZIO.acquireRelease(ZIO.attemptBlocking(hikariDataSource.getConnection).refineToOrDie[SQLException])(con => ZIO.attemptBlocking(hikariDataSource.evictConnection(con)).orDie diff --git a/jdbc-hikaricp/src/test/scala/zio/sql/HikariConnectionPoolSpec.scala b/jdbc-hikaricp/src/test/scala/zio/sql/HikariConnectionPoolSpec.scala index 31a2a5f5b..d2870c0a2 100644 --- a/jdbc-hikaricp/src/test/scala/zio/sql/HikariConnectionPoolSpec.scala +++ b/jdbc-hikaricp/src/test/scala/zio/sql/HikariConnectionPoolSpec.scala @@ -119,7 +119,7 @@ object HikariConnectionPoolSpec extends ZIOSpecDefault { ) } @@ timeout(10.seconds) @@ withLiveClock, test("connection init SQL should be configurable") { - val initialSql = "SELECT 1 FROM test.test" + val initialSql = "SELECT 1" (for { cp <- ZIO.service[HikariConnectionPool] } yield assertTrue(cp.dataSource.getConnectionInitSql == initialSql)) diff --git a/jdbc-hikaricp/src/test/scala/zio/sql/MySqlTestContainer.scala b/jdbc-hikaricp/src/test/scala/zio/sql/MySqlTestContainer.scala index 16ec7cc17..ebba6fa31 100644 --- a/jdbc-hikaricp/src/test/scala/zio/sql/MySqlTestContainer.scala +++ b/jdbc-hikaricp/src/test/scala/zio/sql/MySqlTestContainer.scala @@ -12,10 +12,7 @@ object MySqlTestContainer { ZIO.attemptBlocking { val c = new MySQLContainer( mysqlImageVersion = Option(imageName).map(DockerImageName.parse) - ).configure { a => - a.withInitScript("test_schema.sql") - () - } + ) c.start() c } diff --git a/jdbc/src/test/scala/zio/sql/JdbcRunnableSpec.scala b/jdbc/src/test/scala/zio/sql/JdbcRunnableSpec.scala index d72bd8c0c..b4c031db0 100644 --- a/jdbc/src/test/scala/zio/sql/JdbcRunnableSpec.scala +++ b/jdbc/src/test/scala/zio/sql/JdbcRunnableSpec.scala @@ -51,10 +51,11 @@ trait JdbcRunnableSpec extends ZIOSpecDefault with Jdbc { ) } + val connectionPool: ZLayer[Any, Throwable, ConnectionPool] = poolConfigLayer >>> ConnectionPool.live + private[this] final lazy val jdbcLayer: ZLayer[Any, Any, SqlDriver] = ZLayer.make[SqlDriver]( - poolConfigLayer.orDie, - ConnectionPool.live.orDie, + connectionPool.orDie, SqlDriver.live ) @@ -65,7 +66,7 @@ trait JdbcRunnableSpec extends ZIOSpecDefault with Jdbc { def both[A, B](fa: => Gen[R, A], fb: => Gen[R, B]): Gen[R, (A, B)] = fa.zip(fb) } - private[this] def testContainer: ZIO[Scope, Throwable, SingleContainer[_] with JdbcDatabaseContainer] = + val testContainer: ZIO[Scope, Throwable, SingleContainer[_] with JdbcDatabaseContainer] = ZIO.acquireRelease { ZIO.attemptBlocking { val c = getContainer diff --git a/oracle/src/test/scala/zio/sql/oracle/OracleSqlModuleSpec.scala b/oracle/src/test/scala/zio/sql/oracle/OracleSqlModuleSpec.scala index 40007cd4d..dc7869b93 100644 --- a/oracle/src/test/scala/zio/sql/oracle/OracleSqlModuleSpec.scala +++ b/oracle/src/test/scala/zio/sql/oracle/OracleSqlModuleSpec.scala @@ -130,13 +130,13 @@ object OracleSqlModuleSpec extends OracleRunnableSpec with ShopSchema { assertZIO(execute(command))(equalTo(2)) }, test("Can insert all supported types") { - val sqlMinDateTime = LocalDateTime.of(-4713, 1, 1, 0, 0) + val sqlMinDateTime = LocalDateTime.of(1, 1, 1, 0, 0) val sqlMaxDateTime = LocalDateTime.of(9999, 12, 31, 23, 59) val sqlInstant = Gen.instant(sqlMinDateTime.toInstant(ZoneOffset.MIN), sqlMaxDateTime.toInstant(ZoneOffset.MAX)) - val sqlYear = Gen.int(-4713, 9999).filter(_ != 0).map(Year.of) + val sqlYear = Gen.int(1, 9999).map(Year.of) val sqlLocalDate = for { year <- sqlYear @@ -216,6 +216,7 @@ object OracleSqlModuleSpec extends OracleRunnableSpec with ShopSchema { durationCol ).values(row) + // printInsert(insert) // TODO: ensure we can read values back correctly // val read = // select( diff --git a/sqlserver/src/test/scala/zio/sql/sqlserver/SqlServerModuleSpec.scala b/sqlserver/src/test/scala/zio/sql/sqlserver/SqlServerModuleSpec.scala index e435e2316..30371d83e 100644 --- a/sqlserver/src/test/scala/zio/sql/sqlserver/SqlServerModuleSpec.scala +++ b/sqlserver/src/test/scala/zio/sql/sqlserver/SqlServerModuleSpec.scala @@ -656,7 +656,7 @@ object SqlServerModuleSpec extends SqlServerRunnableSpec with DbSchema { Gen.option(Gen.int), sqlLocalDate, sqlLocalDateTime, - Gen.localTime, + Gen.localTime.map(normLt), // needs to be truncated before saving to the db for predictable outcome Gen.long, sqlOffsetDateTime, sqlOffsetTime, From 05229b129690f8cd641bbceeb5fa1151ea1225a5 Mon Sep 17 00:00:00 2001 From: jczuchnowski Date: Wed, 28 Sep 2022 22:32:53 +0200 Subject: [PATCH 6/8] Downgrade Hikari CPto a version supporting Java 1.8 --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 5941858bb..52595bbbc 100644 --- a/build.sbt +++ b/build.sbt @@ -138,7 +138,7 @@ lazy val jdbc_hikaricp = project .settings(buildInfoSettings("zio.sql.jdbc-hickaricp")) .settings( libraryDependencies ++= Seq( - "com.zaxxer" % "HikariCP" % "5.0.1", + "com.zaxxer" % "HikariCP" % "4.0.3", // 5.x doesn't support Java 1.8 "dev.zio" %% "zio-test" % zioVersion % Test, "dev.zio" %% "zio-test-sbt" % zioVersion % Test, "org.testcontainers" % "mysql" % testcontainersVersion % Test, From 007ebc190c9b6bd9b7c655e83d97dd1263fe2a6a Mon Sep 17 00:00:00 2001 From: jczuchnowski Date: Thu, 29 Sep 2022 13:56:34 +0200 Subject: [PATCH 7/8] Update zio-schema to 0.2.1 --- build.sbt | 2 +- mysql/src/main/scala/zio/sql/mysql/MysqlRenderModule.scala | 5 +++-- mysql/src/test/scala/zio/sql/mysql/MysqlModuleSpec.scala | 1 + .../src/main/scala/zio/sql/oracle/OracleRenderModule.scala | 4 ++-- .../src/test/scala/zio/sql/oracle/OracleSqlModuleSpec.scala | 3 ++- .../scala/zio/sql/postgresql/PostgresRenderModule.scala | 5 +++-- .../scala/zio/sql/postgresql/PostgresSqlModuleSpec.scala | 6 +++++- .../scala/zio/sql/sqlserver/SqlServerRenderModule.scala | 5 +++-- .../test/scala/zio/sql/sqlserver/SqlServerModuleSpec.scala | 1 + 9 files changed, 21 insertions(+), 11 deletions(-) diff --git a/build.sbt b/build.sbt index e79e72b2c..e6ff3719a 100644 --- a/build.sbt +++ b/build.sbt @@ -24,7 +24,7 @@ addCommandAlias("fmt", "fmtOnce;fmtOnce") addCommandAlias("check", "all scalafmtSbtCheck scalafmtCheck test:scalafmtCheck") val zioVersion = "2.0.2" -val zioSchemaVersion = "0.2.0" +val zioSchemaVersion = "0.2.1" val testcontainersVersion = "1.17.3" val testcontainersScalaVersion = "0.40.10" val logbackVersion = "1.2.11" diff --git a/mysql/src/main/scala/zio/sql/mysql/MysqlRenderModule.scala b/mysql/src/main/scala/zio/sql/mysql/MysqlRenderModule.scala index 17481a48f..1811b7879 100644 --- a/mysql/src/main/scala/zio/sql/mysql/MysqlRenderModule.scala +++ b/mysql/src/main/scala/zio/sql/mysql/MysqlRenderModule.scala @@ -138,7 +138,7 @@ trait MysqlRenderModule extends MysqlSqlModule { self => private def renderInsertValue[Z](z: Z)(implicit render: Renderer, schema: Schema[Z]): Unit = schema.toDynamic(z) match { - case DynamicValue.Record(listMap) => + case DynamicValue.Record(_, listMap) => listMap.values.toList match { case head :: Nil => renderDynamicValue(head) case head :: next => @@ -147,7 +147,7 @@ trait MysqlRenderModule extends MysqlSqlModule { self => renderDynamicValues(next) case Nil => () } - case value => renderDynamicValue(value) + case value => renderDynamicValue(value) } private def renderDynamicValues(dynValues: List[DynamicValue])(implicit render: Renderer): Unit = @@ -170,6 +170,7 @@ trait MysqlRenderModule extends MysqlSqlModule { self => render(value) case StandardType.InstantType(formatter) => render(s"'${formatter.format(value.asInstanceOf[Instant])}'") + case ByteType => render(s"'${value}'") case CharType => render(s"'${value}'") case IntType => render(value) case StandardType.MonthDayType => render(s"'${value}'") diff --git a/mysql/src/test/scala/zio/sql/mysql/MysqlModuleSpec.scala b/mysql/src/test/scala/zio/sql/mysql/MysqlModuleSpec.scala index 8308060e5..6bebe34d3 100644 --- a/mysql/src/test/scala/zio/sql/mysql/MysqlModuleSpec.scala +++ b/mysql/src/test/scala/zio/sql/mysql/MysqlModuleSpec.scala @@ -203,6 +203,7 @@ object MysqlModuleSpec extends MysqlRunnableSpec with ShopSchema { ) implicit val customerRowSchema = Schema.CaseClass5[UUID, LocalDate, String, String, Boolean, CustomerRow]( + TypeId.parse("zio.sql.mysql.CustomerRow"), Schema.Field("id", Schema.primitive[UUID](zio.schema.StandardType.UUIDType)), Schema.Field( "dateOfBirth", diff --git a/oracle/src/main/scala/zio/sql/oracle/OracleRenderModule.scala b/oracle/src/main/scala/zio/sql/oracle/OracleRenderModule.scala index c874eba74..9176b119e 100644 --- a/oracle/src/main/scala/zio/sql/oracle/OracleRenderModule.scala +++ b/oracle/src/main/scala/zio/sql/oracle/OracleRenderModule.scala @@ -427,7 +427,7 @@ trait OracleRenderModule extends OracleSqlModule { self => def renderInsertValue[Z](z: Z, builder: StringBuilder)(implicit schema: Schema[Z]): Unit = schema.toDynamic(z) match { - case DynamicValue.Record(listMap) => + case DynamicValue.Record(_, listMap) => listMap.values.toList match { case head :: Nil => renderDynamicValue(head, builder) case head :: next => @@ -436,7 +436,7 @@ trait OracleRenderModule extends OracleSqlModule { self => renderDynamicValues(next, builder) case Nil => () } - case value => renderDynamicValue(value, builder) + case value => renderDynamicValue(value, builder) } def renderDynamicValue(dynValue: DynamicValue, builder: StringBuilder): Unit = diff --git a/oracle/src/test/scala/zio/sql/oracle/OracleSqlModuleSpec.scala b/oracle/src/test/scala/zio/sql/oracle/OracleSqlModuleSpec.scala index dc7869b93..2eff8468f 100644 --- a/oracle/src/test/scala/zio/sql/oracle/OracleSqlModuleSpec.scala +++ b/oracle/src/test/scala/zio/sql/oracle/OracleSqlModuleSpec.scala @@ -7,7 +7,7 @@ import zio.test._ import scala.language.postfixOps import java.util.UUID import java.time.format.DateTimeFormatter -import zio.schema.Schema +import zio.schema.{ Schema, TypeId } import zio.prelude._ import java.time.{ LocalDate, LocalDateTime, Month, Year, YearMonth, ZoneOffset, ZonedDateTime } @@ -75,6 +75,7 @@ object OracleSqlModuleSpec extends OracleRunnableSpec with ShopSchema { ) implicit val customerRowSchema = Schema.CaseClass5[UUID, LocalDate, String, String, Boolean, CustomerRow]( + TypeId.parse("zio.sql.oracle.CustomerRow"), Schema.Field("id", Schema.primitive[UUID](zio.schema.StandardType.UUIDType)), Schema.Field( "dateOfBirth", diff --git a/postgres/src/main/scala/zio/sql/postgresql/PostgresRenderModule.scala b/postgres/src/main/scala/zio/sql/postgresql/PostgresRenderModule.scala index 88655dc20..7aad08d3b 100644 --- a/postgres/src/main/scala/zio/sql/postgresql/PostgresRenderModule.scala +++ b/postgres/src/main/scala/zio/sql/postgresql/PostgresRenderModule.scala @@ -64,7 +64,7 @@ trait PostgresRenderModule extends PostgresSqlModule { self => private def renderInserValue[Z](z: Z)(implicit render: Renderer, schema: Schema[Z]): Unit = schema.toDynamic(z) match { - case DynamicValue.Record(listMap) => + case DynamicValue.Record(_, listMap) => listMap.values.toList match { case head :: Nil => renderDynamicValue(head) case head :: next => @@ -73,7 +73,7 @@ trait PostgresRenderModule extends PostgresSqlModule { self => renderDynamicValues(next) case Nil => () } - case value => renderDynamicValue(value) + case value => renderDynamicValue(value) } private def renderDynamicValues(dynValues: List[DynamicValue])(implicit render: Renderer): Unit = @@ -98,6 +98,7 @@ trait PostgresRenderModule extends PostgresSqlModule { self => render(value) case StandardType.InstantType(formatter) => render(s"'${formatter.format(value.asInstanceOf[Instant])}'") + case ByteType => render(s"'${value}'") case CharType => render(s"'${value}'") case IntType => render(value) case StandardType.MonthDayType => render(s"'${value}'") diff --git a/postgres/src/test/scala/zio/sql/postgresql/PostgresSqlModuleSpec.scala b/postgres/src/test/scala/zio/sql/postgresql/PostgresSqlModuleSpec.scala index f5e47982d..11795047f 100644 --- a/postgres/src/test/scala/zio/sql/postgresql/PostgresSqlModuleSpec.scala +++ b/postgres/src/test/scala/zio/sql/postgresql/PostgresSqlModuleSpec.scala @@ -8,7 +8,7 @@ import zio.test._ import java.time._ import java.util.UUID import scala.language.postfixOps -import zio.schema.Schema +import zio.schema.{ Schema, TypeId } import java.time.format.DateTimeFormatter object PostgresSqlModuleSpec extends PostgresRunnableSpec with DbSchema { @@ -424,6 +424,7 @@ object PostgresSqlModuleSpec extends PostgresRunnableSpec with DbSchema { implicit val customerRowSchema = Schema.CaseClass7[UUID, String, String, Boolean, LocalDate, String, ZonedDateTime, CustomerRow]( + TypeId.parse("zio.sql.postgres.CustomerRow"), Schema.Field("id", Schema.primitive[UUID](zio.schema.StandardType.UUIDType)), Schema.Field("firstName", Schema.primitive[String](zio.schema.StandardType.StringType)), Schema.Field("lastName", Schema.primitive[String](zio.schema.StandardType.StringType)), @@ -480,6 +481,7 @@ object PostgresSqlModuleSpec extends PostgresRunnableSpec with DbSchema { final case class InputOrders(uuid: UUID, customerId: UUID, localDate: LocalDate) implicit val inputOrdersSchema = Schema.CaseClass3[UUID, UUID, LocalDate, InputOrders]( + TypeId.parse("zio.sql.postgres.InputOrders"), Schema.Field("uuid", Schema.primitive[UUID](zio.schema.StandardType.UUIDType)), Schema.Field("customerId", Schema.primitive[UUID](zio.schema.StandardType.UUIDType)), Schema.Field( @@ -532,6 +534,7 @@ object PostgresSqlModuleSpec extends PostgresRunnableSpec with DbSchema { .transform(bigDec => new BigDecimal(bigDec, java.math.MathContext.DECIMAL128), _.bigDecimal) implicit val orderDetailsRowSchema = Schema.CaseClass4[UUID, UUID, Int, BigDecimal, OrderDetailsRow]( + TypeId.parse("zio.sql.postgres.OrderDetailsRow"), Schema.Field("orderId", Schema.primitive[UUID](zio.schema.StandardType.UUIDType)), Schema.Field("productId", Schema.primitive[UUID](zio.schema.StandardType.UUIDType)), Schema.Field("quantity", Schema.primitive[Int](zio.schema.StandardType.IntType)), @@ -624,6 +627,7 @@ object PostgresSqlModuleSpec extends PostgresRunnableSpec with DbSchema { ) implicit val personSchema = Schema.CaseClass4[UUID, String, String, Option[LocalDate], Person]( + TypeId.parse("zio.sql.postgres.Person"), Schema.Field("id", Schema.primitive[UUID](zio.schema.StandardType.UUIDType)), Schema.Field("firstName", Schema.primitive[String](zio.schema.StandardType.StringType)), Schema.Field("lastName", Schema.primitive[String](zio.schema.StandardType.StringType)), diff --git a/sqlserver/src/main/scala/zio/sql/sqlserver/SqlServerRenderModule.scala b/sqlserver/src/main/scala/zio/sql/sqlserver/SqlServerRenderModule.scala index 4e5b7cb9d..69e8bf877 100644 --- a/sqlserver/src/main/scala/zio/sql/sqlserver/SqlServerRenderModule.scala +++ b/sqlserver/src/main/scala/zio/sql/sqlserver/SqlServerRenderModule.scala @@ -433,7 +433,7 @@ trait SqlServerRenderModule extends SqlServerSqlModule { self => private def buildInsertValue[Z](z: Z)(implicit render: Renderer, schema: Schema[Z]): Unit = schema.toDynamic(z) match { - case DynamicValue.Record(listMap) => + case DynamicValue.Record(_, listMap) => listMap.values.toList match { case head :: Nil => buildDynamicValue(head) case head :: next => @@ -442,7 +442,7 @@ trait SqlServerRenderModule extends SqlServerSqlModule { self => buildDynamicValues(next) case Nil => () } - case value => buildDynamicValue(value) + case value => buildDynamicValue(value) } private def buildDynamicValues(dynValues: List[DynamicValue])(implicit render: Renderer): Unit = @@ -467,6 +467,7 @@ trait SqlServerRenderModule extends SqlServerSqlModule { self => render(value) case StandardType.InstantType(formatter) => render(s"'${formatter.format(value.asInstanceOf[Instant])}'") + case ByteType => render(s"${value}") case CharType => render(s"N'${value}'") case IntType => render(value) case StandardType.MonthDayType => render(s"'${value}'") diff --git a/sqlserver/src/test/scala/zio/sql/sqlserver/SqlServerModuleSpec.scala b/sqlserver/src/test/scala/zio/sql/sqlserver/SqlServerModuleSpec.scala index 30371d83e..d0d3fe4eb 100644 --- a/sqlserver/src/test/scala/zio/sql/sqlserver/SqlServerModuleSpec.scala +++ b/sqlserver/src/test/scala/zio/sql/sqlserver/SqlServerModuleSpec.scala @@ -546,6 +546,7 @@ object SqlServerModuleSpec extends SqlServerRunnableSpec with DbSchema { ) implicit val customerRowSchema = Schema.CaseClass5[UUID, String, String, Boolean, LocalDate, CustomerRow]( + TypeId.parse("zio.sql.sqlserver.CustomerRow"), Schema.Field("id", Schema.primitive[UUID](zio.schema.StandardType.UUIDType)), Schema.Field("firstName", Schema.primitive[String](zio.schema.StandardType.StringType)), Schema.Field("lastName", Schema.primitive[String](zio.schema.StandardType.StringType)), From 9c57450df812457a904259d2c4f8d9ce43db7902 Mon Sep 17 00:00:00 2001 From: jczuchnowski Date: Sun, 2 Oct 2022 20:02:51 +0200 Subject: [PATCH 8/8] Fix Byte array serialization for Oracle and SqlServer --- .../main/scala/zio/sql/oracle/OracleRenderModule.scala | 9 +++++++-- .../scala/zio/sql/postgresql/PostgresRenderModule.scala | 2 +- .../scala/zio/sql/sqlserver/SqlServerRenderModule.scala | 7 ++++++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/oracle/src/main/scala/zio/sql/oracle/OracleRenderModule.scala b/oracle/src/main/scala/zio/sql/oracle/OracleRenderModule.scala index 9176b119e..ffb05cd54 100644 --- a/oracle/src/main/scala/zio/sql/oracle/OracleRenderModule.scala +++ b/oracle/src/main/scala/zio/sql/oracle/OracleRenderModule.scala @@ -467,8 +467,8 @@ trait OracleRenderModule extends OracleSqlModule { self => val chunk = value.asInstanceOf[Chunk[Object]] builder.append("'") for (b <- chunk) - builder.append(String.format("%02x", b)) - builder.append(s"'") + builder.append("%02x".format(b)) + builder.append("'") () case StandardType.LocalDateTimeType(formatter) => builder.append( @@ -557,6 +557,11 @@ trait OracleRenderModule extends OracleSqlModule { self => case DynamicValue.NoneValue => builder.append("null") () + case DynamicValue.Sequence(chunk) => + builder.append("'") + for (DynamicValue.Primitive(v, _) <- chunk) + builder.append("%02x".format(v)) + val _ = builder.append("'") case _ => () } diff --git a/postgres/src/main/scala/zio/sql/postgresql/PostgresRenderModule.scala b/postgres/src/main/scala/zio/sql/postgresql/PostgresRenderModule.scala index 7aad08d3b..bf9e63464 100644 --- a/postgres/src/main/scala/zio/sql/postgresql/PostgresRenderModule.scala +++ b/postgres/src/main/scala/zio/sql/postgresql/PostgresRenderModule.scala @@ -202,7 +202,7 @@ trait PostgresRenderModule extends PostgresSqlModule { self => } case TByteArray => render( - lit.value.asInstanceOf[Chunk[Byte]].map("""\%03o""" format _).mkString("E\'", "", "\'") + lit.value.asInstanceOf[Chunk[Byte]].map("""\%03o""".format(_)).mkString("E\'", "", "\'") ) // todo fix `cast` infers correctly but map doesn't work for some reason case tt @ TChar => render("'", tt.cast(lit.value), "'") // todo is this the same as a string? fix escaping diff --git a/sqlserver/src/main/scala/zio/sql/sqlserver/SqlServerRenderModule.scala b/sqlserver/src/main/scala/zio/sql/sqlserver/SqlServerRenderModule.scala index 69e8bf877..d437539f0 100644 --- a/sqlserver/src/main/scala/zio/sql/sqlserver/SqlServerRenderModule.scala +++ b/sqlserver/src/main/scala/zio/sql/sqlserver/SqlServerRenderModule.scala @@ -475,7 +475,7 @@ trait SqlServerRenderModule extends SqlServerSqlModule { self => val chunk = value.asInstanceOf[Chunk[Object]] render("CONVERT(VARBINARY(MAX),'") for (b <- chunk) - render(String.format("%02x", b)) + render("%02x".format(b)) render("', 2)") case StandardType.MonthType => render(s"'${value}'") case StandardType.LocalDateTimeType(formatter) => @@ -521,6 +521,11 @@ trait SqlServerRenderModule extends SqlServerSqlModule { self => buildDynamicValue(right) case DynamicValue.SomeValue(value) => buildDynamicValue(value) case DynamicValue.NoneValue => render("null") + case DynamicValue.Sequence(chunk) => + render("CONVERT(VARBINARY(MAX),'") + for (DynamicValue.Primitive(v, _) <- chunk) + render("%02x".format(v)) + render("', 2)") case _ => () }