diff --git a/README.md b/README.md index eb70172dd..e7ffe5fc6 100644 --- a/README.md +++ b/README.md @@ -207,12 +207,12 @@ lazy val yourProject = project.in(file("app")) .settings( libraryDependencies ++= Seq( // One or more of: - "org.scalamolecule" %%% "molecule-sql-postgres" % "0.14.0", - "org.scalamolecule" %%% "molecule-sql-sqlite" % "0.14.0", - "org.scalamolecule" %%% "molecule-sql-mysql" % "0.14.0", - "org.scalamolecule" %%% "molecule-sql-mariadb" % "0.14.0", - "org.scalamolecule" %%% "molecule-sql-h2" % "0.14.0", - "org.scalamolecule" %%% "molecule-datalog-datomic" % "0.14.0", + "org.scalamolecule" %%% "molecule-sql-postgres" % "0.14.1", + "org.scalamolecule" %%% "molecule-sql-sqlite" % "0.14.1", + "org.scalamolecule" %%% "molecule-sql-mysql" % "0.14.1", + "org.scalamolecule" %%% "molecule-sql-mariadb" % "0.14.1", + "org.scalamolecule" %%% "molecule-sql-h2" % "0.14.1", + "org.scalamolecule" %%% "molecule-datalog-datomic" % "0.14.1", ), moleculeSchemas := Seq("app/dataModel") // paths to directories with Data Model definition files ) diff --git a/build.sbt b/build.sbt index ac3f978ff..b28ee1598 100644 --- a/build.sbt +++ b/build.sbt @@ -19,7 +19,7 @@ inThisBuild( organizationName := "ScalaMolecule", organizationHomepage := Some(url("http://www.scalamolecule.org")), versionScheme := Some("early-semver"), - version := "0.14.0", + version := "0.14.1", scalaVersion := scala213, crossScalaVersions := allScala, diff --git a/core/shared/src/main/scala/molecule/core/api/Api_async.scala b/core/shared/src/main/scala/molecule/core/api/Api_async.scala index 8def9544e..3931b80f1 100644 --- a/core/shared/src/main/scala/molecule/core/api/Api_async.scala +++ b/core/shared/src/main/scala/molecule/core/api/Api_async.scala @@ -100,11 +100,13 @@ trait Api_async_transact { api: Api_async with Spi_async => def unitOfWork[T](body: => Future[T]) (implicit conn: Conn, ec: EC): Future[T] = { + conn.setInsideUOW(true) conn.waitCommitting() body .map { t => // Commit all actions conn.commit() + conn.setInsideUOW(false) t } .recover { case e => diff --git a/core/shared/src/main/scala/molecule/core/api/Api_io.scala b/core/shared/src/main/scala/molecule/core/api/Api_io.scala index ef0548097..417b2dd95 100644 --- a/core/shared/src/main/scala/molecule/core/api/Api_io.scala +++ b/core/shared/src/main/scala/molecule/core/api/Api_io.scala @@ -93,11 +93,13 @@ trait Api_io_transact { api: Api_io with Spi_io => def unitOfWork[T](body: => IO[T])(implicit conn: Conn): IO[T] = { + conn.setInsideUOW(true) conn.waitCommitting() body.attempt.map { case Right(t) => // Commit all actions conn.commit() + conn.setInsideUOW(false) t case Left(error: Throwable) => // Rollback all executed actions so far diff --git a/core/shared/src/main/scala/molecule/core/api/Api_sync.scala b/core/shared/src/main/scala/molecule/core/api/Api_sync.scala index e53f651f4..449aaa7d1 100644 --- a/core/shared/src/main/scala/molecule/core/api/Api_sync.scala +++ b/core/shared/src/main/scala/molecule/core/api/Api_sync.scala @@ -87,11 +87,13 @@ trait Api_sync_transact { api: Api_sync with Spi_sync => def unitOfWork[T](body: => T)(implicit conn: Conn): T = { + conn.setInsideUOW(true) conn.waitCommitting() try { val result = body // Commit all actions conn.commit() + conn.setInsideUOW(false) result } catch { case NonFatal(e) => diff --git a/core/shared/src/main/scala/molecule/core/api/Api_zio.scala b/core/shared/src/main/scala/molecule/core/api/Api_zio.scala index 91cac996a..cfc29477a 100644 --- a/core/shared/src/main/scala/molecule/core/api/Api_zio.scala +++ b/core/shared/src/main/scala/molecule/core/api/Api_zio.scala @@ -80,11 +80,13 @@ trait Api_zio_transact { api: Api_zio with Spi_zio => def unitOfWork[T](body: => ZIO[Conn, MoleculeError, T]): ZIO[Conn, MoleculeError, T] = { for { conn <- ZIO.service[Conn] + _ = conn.setInsideUOW(true) _ = conn.waitCommitting() result <- body .map { t => // Commit all actions conn.commit() + conn.setInsideUOW(false) t } .mapError { error => diff --git a/core/shared/src/main/scala/molecule/core/spi/Conn.scala b/core/shared/src/main/scala/molecule/core/spi/Conn.scala index b65dbedbc..bdc1493f7 100644 --- a/core/shared/src/main/scala/molecule/core/spi/Conn.scala +++ b/core/shared/src/main/scala/molecule/core/spi/Conn.scala @@ -12,11 +12,6 @@ import scala.concurrent.{ExecutionContext, Future} abstract class Conn(val proxy: ConnProxy) extends ModelUtils { self: DataType => - protected var commit_ = true - - def waitCommitting(): Unit = ??? - def commit(): Unit = ??? - def rollback(): Unit = ??? def transact_async(data: Data) (implicit ec: ExecutionContext): Future[TxReport] = @@ -36,7 +31,7 @@ abstract class Conn(val proxy: ConnProxy) ExecutionError(s"`$method` only implemented on JVM platform.") - // Subscriptions -------------------------------------------------------------- + // Subscriptions ------------------------------------------------------------- private var callbacks = List.empty[(List[Element], (Set[String], Boolean) => Future[Unit])] @@ -57,6 +52,16 @@ abstract class Conn(val proxy: ConnProxy) callbacks = callbacks.filterNot(_._1 == elements) } + + // Transaction handling ------------------------------------------------------ + + private var uow_ = false + protected var commit_ = true + + def waitCommitting(): Unit = ??? + def commit(): Unit = ??? + def rollback(): Unit = ??? + def savepoint_sync[T](body: Savepoint => T): T = ??? def savepoint_async[T](body: Savepoint => Future[T]) (implicit ec: ExecutionContext): Future[T] = ??? @@ -67,7 +72,11 @@ abstract class Conn(val proxy: ConnProxy) def savepoint_io[T](body: Savepoint => IO[T]): IO[T] = ??? - def hasSavepoint: Boolean = ??? + + def setInsideUOW(inside: Boolean): Unit = uow_ = inside + def isInsideUOW: Boolean = uow_ + + def isInsideSavepoint: Boolean = ??? def setAutoCommit(bool: Boolean): Unit = ??? } diff --git a/coreTests/jvm/src/test/scala/molecule/coreTests/spi/transaction/Transactions_async.scala b/coreTests/jvm/src/test/scala/molecule/coreTests/spi/transaction/Transactions_async.scala index 8535ffb31..73308bdd1 100644 --- a/coreTests/jvm/src/test/scala/molecule/coreTests/spi/transaction/Transactions_async.scala +++ b/coreTests/jvm/src/test/scala/molecule/coreTests/spi/transaction/Transactions_async.scala @@ -132,14 +132,21 @@ trait Transactions_async extends CoreTestSuite with Api_async with Api_async_tra } - "mixed, unified actions" - types { implicit conn => + "mixed with queries" - types { implicit conn => for { _ <- unitOfWork { for { _ <- Ns.int(1).save.transact + _ <- Ns.int.query.get.map(_ ==> List(1)) + _ <- Ns.int.insert(2, 3).transact + _ <- Ns.int.query.get.map(_ ==> List(1, 2, 3)) + _ <- Ns(1).delete.transact + _ <- Ns.int.query.get.map(_ ==> List(2, 3)) + _ <- Ns(3).int.*(10).update.transact + _ <- Ns.int.query.get.map(_ ==> List(2, 30)) } yield () } _ <- Ns.int.query.get.map(_ ==> List(2, 30)) @@ -147,24 +154,101 @@ trait Transactions_async extends CoreTestSuite with Api_async with Api_async_tra } - "mixed, with queries" - types { implicit conn => + "abort save" - types { implicit conn => for { + _ <- Ns.int(1).save.transact _ <- unitOfWork { for { - _ <- Ns.int(1).save.transact - _ <- Ns.int.query.get.map(_ ==> List(1)) + _ <- Ns.int(2).save.transact + _ <- Ns.int.query.get.map(_ ==> List(1, 2)) + _ = throw new Exception() + } yield () + }.recover { + case _: Exception => () + } + _ <- Ns.int.query.get.map(_ ==> List(1)) + } yield () + } + + "abort insert" - types { implicit conn => + for { + _ <- Ns.int(1).save.transact + _ <- unitOfWork { + for { _ <- Ns.int.insert(2, 3).transact _ <- Ns.int.query.get.map(_ ==> List(1, 2, 3)) + _ = throw new Exception() + } yield () + }.recover { + case _: Exception => () + } + _ <- Ns.int.query.get.map(_ ==> List(1)) + } yield () + } + + + "abort update" - types { implicit conn => + for { + _ <- Ns.int(1).save.transact + _ <- unitOfWork { + for { + _ <- Ns(1).int.*(10).update.transact + _ <- Ns.int.query.get.map(_ ==> List(10)) + _ = throw new Exception() + } yield () + }.recover { + case _: Exception => () + } + _ <- Ns.int.query.get.map(_ ==> List(1)) + } yield () + } + + "abort delete" - types { implicit conn => + for { + _ <- Ns.int.insert(1, 2).transact + _ <- unitOfWork { + for { _ <- Ns(1).delete.transact - _ <- Ns.int.query.get.map(_ ==> List(2, 3)) + _ <- Ns.int.query.get.map(_ ==> List(1, 2)) + _ = throw new Exception() + } yield () + }.recover { + case _: Exception => () + } + _ <- Ns.int.query.get.map(_ ==> List(1, 2)) + } yield () + } - _ <- Ns(3).int.*(10).update.transact - _ <- Ns.int.query.get.map(_ ==> List(2, 30)) + + "abort mixed" - types { implicit conn => + for { + // Initial data + _ <- Ns.int(1).save.transact + + _ <- unitOfWork { + for { + _ <- Ns.int(2).save.transact + _ <- Ns.int.query.get.map(_ ==> List(1, 2)) + + _ <- Ns.int.insert(3, 4).transact + _ <- Ns.int.query.get.map(_ ==> List(1, 2, 3, 4)) + + _ <- Ns(2).delete.transact + _ <- Ns.int.query.get.map(_ ==> List(1, 3, 4)) + + _ <- Ns(4).int.*(10).update.transact + _ <- Ns.int.query.get.map(_ ==> List(1, 3, 40)) + + _ = throw new Exception() } yield () + }.recover { + case _: Exception => () } - _ <- Ns.int.query.get.map(_ ==> List(2, 30)) + + // Initial data remains intact + _ <- Ns.int.query.get.map(_ ==> List(1)) } yield () } @@ -273,7 +357,7 @@ trait Transactions_async extends CoreTestSuite with Api_async with Api_async_tra } } - // Without rollbacks, the above is the same as the following: + // Without rollbacks, the above is the same as the following: "commit2" - types { implicit conn => for { _ <- Ns.int.insert(1 to 4).transact diff --git a/coreTests/jvm/src/test/scala/molecule/coreTests/spi/transaction/Transactions_sync.scala b/coreTests/jvm/src/test/scala/molecule/coreTests/spi/transaction/Transactions_sync.scala index 9e5ac2010..0c25ac13f 100644 --- a/coreTests/jvm/src/test/scala/molecule/coreTests/spi/transaction/Transactions_sync.scala +++ b/coreTests/jvm/src/test/scala/molecule/coreTests/spi/transaction/Transactions_sync.scala @@ -119,18 +119,7 @@ trait Transactions_sync extends CoreTestSuite with Api_sync with Api_sync_transa } - "mixed, unified actions" - types { implicit conn => - unitOfWork { - Ns.int(1).save.transact - Ns.int.insert(2, 3).transact - Ns(1).delete.transact - Ns(3).int.*(10).update.transact - } - Ns.int.query.get ==> List(2, 30) - } - - - "mixed, with queries" - types { implicit conn => + "mixed with queries" - types { implicit conn => unitOfWork { Ns.int(1).save.transact Ns.int.query.get ==> List(1) @@ -148,6 +137,95 @@ trait Transactions_sync extends CoreTestSuite with Api_sync with Api_sync_transa } + "abort save" - types { implicit conn => + Ns.int(1).save.transact + try { + unitOfWork { + Ns.int(2).save.transact + Ns.int.query.get ==> List(1, 2) + throw new Exception() + } + } catch { + case _: Exception => () + } + Ns.int.query.get ==> List(1) + } + + + "abort insert" - types { implicit conn => + Ns.int(1).save.transact + try { + unitOfWork { + Ns.int.insert(2, 3).transact + Ns.int.query.get ==> List(1, 2, 3) + throw new Exception() + } + } catch { + case _: Exception => () + } + Ns.int.query.get ==> List(1) + } + + + "abort update" - types { implicit conn => + Ns.int(1).save.transact + try { + unitOfWork { + Ns(1).int.*(10).update.transact + Ns.int.query.get ==> List(10) + throw new Exception() + } + } catch { + case _: Exception => () + } + Ns.int.query.get ==> List(1) + } + + + "abort delete" - types { implicit conn => + Ns.int.insert(1, 2).transact + try { + unitOfWork { + Ns(1).delete.transact + Ns.int.query.get ==> List(2) + throw new Exception() + } + } catch { + case _: Exception => () + } + Ns.int.query.get ==> List(1, 2) + } + + + "abort mixed" - types { implicit conn => + // Initial data + Ns.int(1).save.transact + + try { + unitOfWork { + Ns.int(2).save.transact + Ns.int.query.get ==> List(1, 2) + + Ns.int.insert(3, 4).transact + Ns.int.query.get ==> List(1, 2, 3, 4) + + Ns(2).delete.transact + Ns.int.query.get ==> List(1, 3, 4) + + Ns(4).int.*(10).update.transact + Ns.int.query.get ==> List(1, 3, 40) + + throw new Exception() + } + } catch { + case _: Exception => () + } + + // Initial data remains intact + Ns.int.query.get ==> List(1) + } + + "money transfer" - types { implicit conn => // Initial balance in two bank accounts Ns.s("fromAccount").int(100).save.transact @@ -175,7 +253,7 @@ trait Transactions_sync extends CoreTestSuite with Api_sync with Api_sync_transa // No data transacted Ns.s.int.query.get ==> List( ("fromAccount", 100), - ("toAccount", 501), + ("toAccount", 50), ) } @@ -319,6 +397,8 @@ trait Transactions_sync extends CoreTestSuite with Api_sync with Api_sync_transa // 1-3 deleted withing uow Ns.int(count).query.get.head ==> 4 } + + Ns.int(count).query.get.head ==> 4 } diff --git a/datalog/datomic/jvm/src/main/scala/molecule/datalog/datomic/facade/DatomicConn_JVM.scala b/datalog/datomic/jvm/src/main/scala/molecule/datalog/datomic/facade/DatomicConn_JVM.scala index 1b5ecbb54..92e77a543 100644 --- a/datalog/datomic/jvm/src/main/scala/molecule/datalog/datomic/facade/DatomicConn_JVM.scala +++ b/datalog/datomic/jvm/src/main/scala/molecule/datalog/datomic/facade/DatomicConn_JVM.scala @@ -35,10 +35,6 @@ case class DatomicConn_JVM( } def optimizeQuery: Boolean = optimizeQueries - override def waitCommitting(): Unit = () - override def commit(): Unit = () - override def rollback(): Unit = () - final def transactEdn(edn: String)(implicit ec: ExecutionContext): Future[TxReport] = { transact_async(readAll(new StringReader(edn)).get(0).asInstanceOf[Data]) } @@ -117,13 +113,4 @@ case class DatomicConn_JVM( ) p.future } - - - override def savepoint_sync[T](body: Savepoint => T): T = ??? - override def savepoint_async[T](body: Savepoint => Future[T]) - (implicit ec: ExecutionContext): Future[T] = ??? - - override def hasSavepoint: Boolean = ??? - - override def setAutoCommit(bool: Boolean): Unit = () } \ No newline at end of file diff --git a/project/releases/v0.14.0.md b/project/releases/v0.14.1.md similarity index 97% rename from project/releases/v0.14.0.md rename to project/releases/v0.14.1.md index 8bb802fbc..d360eae99 100644 --- a/project/releases/v0.14.0.md +++ b/project/releases/v0.14.1.md @@ -1,4 +1,4 @@ -v0.14.0 Transaction handling +v0.14.1 Transaction handling 3 new transaction handling methods are introduced for SQL databases on the jvm side. @@ -106,4 +106,4 @@ Ns.int(count).query.get.head ==> 4 Throwing an exception instead of calling `sp.rollback()` has the same effect. -Using savepoints can be a convenient way of rolling back only parts of a larger body of transactions/`unitOfWork`. Savepoints can even be nested. +Using savepoints can be a convenient way of rolling back only parts of a larger body of transactions/`unitOfWork`. Savepoints can even be nested (see tests). diff --git a/sql/core/jvm/src/main/scala/molecule/sql/core/facade/JdbcConn_JVM.scala b/sql/core/jvm/src/main/scala/molecule/sql/core/facade/JdbcConn_JVM.scala index bfdff3f68..66b331256 100644 --- a/sql/core/jvm/src/main/scala/molecule/sql/core/facade/JdbcConn_JVM.scala +++ b/sql/core/jvm/src/main/scala/molecule/sql/core/facade/JdbcConn_JVM.scala @@ -28,38 +28,6 @@ case class JdbcConn_JVM( val sqlConn: Connection = sqlConn0 - // (Using ListBuffer instead of ArrayDeque for compatibility with Scala 2.12) - val savepointStack = mutable.ListBuffer.empty[java.sql.Savepoint] - - override def waitCommitting(): Unit = { - commit_ = false - } - - override def commit(): Unit = { - commit_ = true - sqlConn.commit() - } - - override def rollback(): Unit = { - commit_ = false - savepointStack.clear() - try { - sqlConn.setAutoCommit(false) - sqlConn.rollback() - logger.info( - "Successfully rolled back" - ) - } catch { - case e: SQLException => - logger.error("Couldn't roll back unsuccessful transaction: " + e) - throw e - case NonFatal(e) => - logger.error("Unexpected rollback error: " + e) - throw e - } - } - - def queryStmt(query: String): PreparedStatement = { sqlConn.prepareStatement( query, @@ -118,6 +86,37 @@ case class JdbcConn_JVM( // Savepoint -------------------------------------------------------- // Thankfully taken from ScalaSql + // (Using ListBuffer instead of ArrayDeque for compatibility with Scala 2.12) + val savepointStack = mutable.ListBuffer.empty[java.sql.Savepoint] + + override def waitCommitting(): Unit = { + commit_ = false + } + + override def commit(): Unit = { + commit_ = true + sqlConn.commit() + } + + override def rollback(): Unit = { + commit_ = false + savepointStack.clear() + try { + sqlConn.setAutoCommit(false) + sqlConn.rollback() + logger.info( + "Successfully rolled back" + ) + } catch { + case e: SQLException => + logger.error("Couldn't roll back unsuccessful transaction: " + e) + throw e + case NonFatal(e) => + logger.error("Unexpected rollback error: " + e) + throw e + } + } + override def savepoint_sync[T](body: Savepoint => T): T = { setAutoCommit(false) val savepoint = sqlConn.setSavepoint() @@ -138,7 +137,6 @@ case class JdbcConn_JVM( } } - override def savepoint_async[T](body: Savepoint => Future[T]) (implicit ec: ExecutionContext): Future[T] = { setAutoCommit(false) @@ -190,7 +188,7 @@ case class JdbcConn_JVM( savepointStack.append(savepoint) val sp = new SavepointImpl(savepoint, () => rollbackSavepoint(savepoint)) body(sp).attempt.map { - case Right(t) => + case Right(t) => if (savepointStack.lastOption.exists(_ eq savepoint)) { // Only release if this savepoint has not been rolled back, // directly or indirectly @@ -210,13 +208,16 @@ case class JdbcConn_JVM( private def rollbackSavepoint(savepoint: java.sql.Savepoint): Unit = { savepointStack.indexOf(savepoint) match { case -1 => // do nothing - case savepointIndex => + case savepointIndex => { sqlConn.rollback(savepointStack(savepointIndex)) - savepointStack.remove(savepointIndex) + savepointStack.remove(savepointIndex, savepointStack.length - savepointIndex) + // Could use takeInPlace instead, but not available on Scala 2.12 + // savepointStack.takeInPlace(savepointIndex) + } } } - override def hasSavepoint: Boolean = savepointStack.nonEmpty + override def isInsideSavepoint: Boolean = savepointStack.nonEmpty override def setAutoCommit(bool: Boolean): Unit = sqlConn.setAutoCommit(false) } \ No newline at end of file diff --git a/sql/core/jvm/src/main/scala/molecule/sql/core/spi/SpiBase_sync.scala b/sql/core/jvm/src/main/scala/molecule/sql/core/spi/SpiBase_sync.scala index c1a72ac86..7437ac78c 100644 --- a/sql/core/jvm/src/main/scala/molecule/sql/core/spi/SpiBase_sync.scala +++ b/sql/core/jvm/src/main/scala/molecule/sql/core/spi/SpiBase_sync.scala @@ -254,11 +254,11 @@ trait SpiBase_sync if (delete.doInspect) delete_inspect(delete) val deleteClean = delete.copy(elements = noKeywords(delete.elements, Some(conn.proxy))) - if (conn.hasSavepoint && hasRef(delete.elements)) { + if (conn.isInsideSavepoint && hasRef(delete.elements)) { throw ModelError("Deletes with relationships not implemented for savepoints.") } - // println(s"------ ${conn.hasSavepoint} ${hasRef(delete.elements)}") - lazy val action = delete_getAction(conn, deleteClean, !conn.hasSavepoint) + // println(s"------ ${conn.isInsideUOW} ${conn.isInsideSavepoint} ${hasRef(delete.elements)}") + lazy val action = delete_getAction(conn, deleteClean, !conn.isInsideUOW && !conn.isInsideSavepoint) val txReport = conn.transact_sync(action) await(conn.callback(delete.elements, true)) txReport @@ -268,7 +268,7 @@ trait SpiBase_sync val conn = conn0.asInstanceOf[JdbcConn_JVM] tryInspect("delete", delete.elements) { val deleteClean = delete.copy(elements = noKeywords(delete.elements, Some(conn.proxy))) - val disableFKS = conn.hasSavepoint && hasRef(delete.elements) + val disableFKS = conn.isInsideSavepoint && hasRef(delete.elements) printInspectTx("DELETE", delete.elements, delete_getAction(conn, deleteClean, disableFKS)) } } diff --git a/sql/core/jvm/src/main/scala/molecule/sql/core/transaction/strategy/delete/DeleteRoot.scala b/sql/core/jvm/src/main/scala/molecule/sql/core/transaction/strategy/delete/DeleteRoot.scala index 31b434a2c..1fbd8d396 100644 --- a/sql/core/jvm/src/main/scala/molecule/sql/core/transaction/strategy/delete/DeleteRoot.scala +++ b/sql/core/jvm/src/main/scala/molecule/sql/core/transaction/strategy/delete/DeleteRoot.scala @@ -54,7 +54,7 @@ case class DeleteRoot( private def executeRoot_sqlite: List[Long] = { if (disableFkConstraints) { - // Turn autoCommit back one to save pragma key before deletions + // Turn autoCommit back on to save pragma key before deletions sqlConn.setAutoCommit(true) val off = sqlConn.prepareStatement("PRAGMA foreign_keys = 0") off.executeUpdate() diff --git a/sql/h2/jvm/src/test/scala/molecule/sql/h2/AdhocJVM_h2.scala b/sql/h2/jvm/src/test/scala/molecule/sql/h2/AdhocJVM_h2.scala index 9531169ce..c1e3861cf 100644 --- a/sql/h2/jvm/src/test/scala/molecule/sql/h2/AdhocJVM_h2.scala +++ b/sql/h2/jvm/src/test/scala/molecule/sql/h2/AdhocJVM_h2.scala @@ -22,7 +22,6 @@ object AdhocJVM_h2 extends TestSuite_h2 { import molecule.coreTests.dataModels.dsl.Types._ implicit val tolerantDouble = tolerantDoubleEquality(toleranceDouble) - println("hello".substring(5, 5)) for { List(a, b) <- Ns.int.insert(1, 2).transact.map(_.ids) _ <- Ns.int(3).save.transact diff --git a/sql/h2/jvm/src/test/scala/molecule/sql/h2/AdhocJVM_h2_sync.scala b/sql/h2/jvm/src/test/scala/molecule/sql/h2/AdhocJVM_h2_sync.scala index b9852a855..2f5637dbf 100644 --- a/sql/h2/jvm/src/test/scala/molecule/sql/h2/AdhocJVM_h2_sync.scala +++ b/sql/h2/jvm/src/test/scala/molecule/sql/h2/AdhocJVM_h2_sync.scala @@ -1,9 +1,6 @@ package molecule.sql.h2 -import molecule.base.error.ValidationErrors -import molecule.core.spi.Conn import molecule.coreTests.dataModels.dsl.Types._ -import molecule.coreTests.dataModels.schema.TypesSchema import molecule.sql.h2.setup.TestSuite_h2 import molecule.sql.h2.sync._ import utest._ @@ -24,765 +21,5 @@ object AdhocJVM_h2_sync extends TestSuite_h2 { } - "transact actions" - { - - "simple" - types { implicit conn => - transact( - Ns.int(1).save, - Ns.int(2).save, - ) - Ns.int.query.get ==> List(1, 2) - } - - - "mixed" - types { implicit conn => - transact( - Ns.int(1).save, // List(1) - Ns.int.insert(2, 3), // List(1, 2, 3) - Ns(1).delete, // List(2, 3) - Ns(3).int.*(10).update, // List(2, 30) - ) - Ns.int.query.get ==> List(2, 30) - } - - - "validation 1" - validation { implicit conn => - import molecule.coreTests.dataModels.dsl.Validation._ - try { - transact( - Type.int(1).save, // not valid - Type.int.insert(4, 5), - ) - } catch { - case ValidationErrors(errorMap) => - errorMap.head._2.head ==> - s"""Type.int with value `1` doesn't satisfy validation: - |_ > 2 - |""".stripMargin - } - - // No data inserted/saved - Type.int.query.get ==> List() - } - - - "validation 2" - validation { implicit conn => - import molecule.coreTests.dataModels.dsl.Validation._ - try { - transact( - Type.int.insert(4, 5), - Type.int(2).save, // not valid - ) - } catch { - case ValidationErrors(errorMap) => - errorMap.head._2.head ==> - s"""Type.int with value `2` doesn't satisfy validation: - |_ > 2 - |""".stripMargin - } - - // No data inserted/saved - Type.int.query.get ==> List() - } - - - "validation 3" - validation { implicit conn => - import molecule.coreTests.dataModels.dsl.Validation._ - transact( - Type.int.insert(4, 5), - Type.int(3).save, - ) - Type.int.a1.query.get ==> List(3, 4, 5) - } - } - - - "unitOfWork" - { - - "simple" - types { implicit conn => - unitOfWork { - Ns.int(1).save.transact - Ns.int(2).save.transact - } - Ns.int.query.get ==> List(1, 2) - } - - - "mixed" - types { implicit conn => - unitOfWork { - Ns.int(1).save.transact // List(1) - Ns.int.insert(2, 3).transact // List(1, 2, 3) - Ns(1).delete.transact // List(2, 3) - Ns(3).int.*(10).update.transact // List(2, 30) - } - Ns.int.query.get ==> List(2, 30) - } - - - "mixed, unified actions" - types { implicit conn => - unitOfWork { - Ns.int(1).save.transact - Ns.int.insert(2, 3).transact - Ns(1).delete.transact - Ns(3).int.*(10).update.transact - } - Ns.int.query.get ==> List(2, 30) - } - - - "mixed, with queries" - types { implicit conn => - unitOfWork { - Ns.int(1).save.transact - Ns.int.query.get ==> List(1) - - Ns.int.insert(2, 3).transact - Ns.int.query.get ==> List(1, 2, 3) - - Ns(1).delete.transact - Ns.int.query.get ==> List(2, 3) - - Ns(3).int.*(10).update.transact - Ns.int.query.get ==> List(2, 30) - } - Ns.int.query.get ==> List(2, 30) - } - - - "money transfer" - types { implicit conn => - // Initial balance in two bank accounts - Ns.s("fromAccount").int(100).save.transact - Ns.s("toAccount").int(50).save.transact - - try { - unitOfWork { - Ns.s_("fromAccount").int.-(200).update.transact - Ns.s_("toAccount").int.+(200).update.transact - - // Check that fromAccount had sufficient funds - if (Ns.s_("fromAccount").int.query.get.head < 0) { - // Abort all transactions in this unit of work - throw new Exception( - "Insufficient funds in fromAccount..." - ) - } - } - } catch { - case e: Exception => - // Do something with failure... - e.getMessage ==> "Insufficient funds in fromAccount..." - } - - // No data transacted - Ns.s.int.query.get ==> List( - ("fromAccount", 100), - ("toAccount", 50), - ) - } - - "money transfer2" - types { implicit conn => - // Initial balance in two bank accounts - Ns.int(100).save.transact - Ns.int(100).query.get ==> List(100) - - try { - unitOfWork { - Ns.int_(100).delete.transact - Ns.int(100).query.get ==> List() - throw new Exception( - "Insufficient funds in fromAccount..." - ) - } - } catch { - case e: Exception => - - e.getMessage ==> "Insufficient funds in fromAccount..." - - } - - // No data transacted - Ns.int.query.get ==> List( - ("fromAccount", 100), - ("toAccount", 50), - ) - } - - - - - // Transfer logic separated with unitOfWork - def transfer(from: String, to: String, amount: Int) - (implicit conn: Conn): Unit = { - unitOfWork { - Ns.s_(from).int.-(amount).update.transact - Ns.s_(to).int.+(amount).update.transact - - val res = Ns.s_(from).int.query.get - val fromBalance = res.head - // Check that fromAccount had sufficient funds - if (fromBalance < 0) { - // Abort all transactions in this unit of work - throw new Exception( - "Insufficient funds in fromAccount..." - ) - } - } - } - - - "money transfer3" - types { implicit conn => - // Initial balance in two bank accounts - Ns.s("fromAccount").int(100).save.transact - Ns.s("toAccount").int(50).save.transact - - try { - transfer("fromAccount", "toAccount", 80) - } catch { - case e: Exception => - // Do something with failure... - e.getMessage ==> "Insufficient funds in fromAccount..." - } - - // Amount transferred - Ns.s.int.a1.query.get ==> List( - ("fromAccount", 20), - ("toAccount", 130), - ) - } - } - - - "savepoint" - { - - "commit" - types { implicit conn => - unitOfWork { - Ns.int.insert(1 to 4).transact - Ns.int(count).query.get.head ==> 4 - savepoint { _ => - // Delete all - Ns.int_.delete.transact - Ns.int(count).query.get.head ==> 0 - } - - // All deleted - Ns.int(count).query.get.head ==> 0 - } - } - - // Without rollbacks, the above is the same as the following: - "commit2" - types { implicit conn => - Ns.int.insert(1 to 4).transact - Ns.int(count).query.get.head ==> 4 - - // Delete all - Ns.int_.delete.transact - Ns.int(count).query.get.head ==> 0 - - // All deleted - Ns.int(count).query.get.head ==> 0 - } - - - "rollback" - types { implicit conn => - unitOfWork { - Ns.int.insert(1 to 4).transact - Ns.int(count).query.get.head ==> 4 - - savepoint { sp => - // Delete all - Ns.int_.delete.transact - Ns.int(count).query.get.head ==> 0 - - // Roll back delete - sp.rollback() - Ns.int(count).query.get.head ==> 4 - } - - // Nothing deleted - Ns.int(count).query.get.head ==> 4 - } - } - - - "rollback2" - types { implicit conn => - Ns.int.insert(1 to 4).transact - Ns.int(count).query.get.head ==> 4 - - unitOfWork { - savepoint { sp => - // Delete all - Ns.int_.delete.transact - Ns.int(count).query.get.head ==> 0 - - // Roll back delete - sp.rollback() - Ns.int(count).query.get.head ==> 4 - } - } - - // Nothing deleted - Ns.int(count).query.get.head ==> 4 - } - - - "throw" - types { implicit conn => - Ns.int.insert(1 to 7).transact - Ns.int(count).query.get.head ==> 7 - - unitOfWork { - Ns.int_.<=(3).delete.transact - Ns.int(count).query.get.head ==> 4 - - try { - savepoint { _ => - // Delete all - Ns.int_.delete.transact - Ns.int(count).query.get.head ==> 0 - - // Roll back delete within savepoint body by throwing exception - throw new Exception("handle error...") - } - } catch { - case e: Exception => e.getMessage ==> "handle error..." - } - - // 1-3 deleted withing uow - Ns.int(count).query.get.head ==> 4 - } - } - - - "throw2" - types { implicit conn => - Ns.int.insert(1 to 7).transact - Ns.int(count).query.get.head ==> 7 - - Ns.int_.<=(3).delete.transact - Ns.int(count).query.get.head ==> 4 - - try { - unitOfWork { - savepoint { _ => - // Delete all - Ns.int_.delete.transact - Ns.int(count).query.get.head ==> 0 - // Roll back delete by throwing exception - throw new Exception("handle error...") - } - } - } catch { - case e: Exception => e.getMessage ==> "handle error..." - } - - // 1-3 deleted in outer context - Ns.int(count).query.get.head ==> 4 - } - - - "throwDouble" - types { implicit conn => - Ns.int.insert(1 to 7).transact - - try { - unitOfWork { - savepoint { outer => - Ns.int(count).query.get.head ==> 7 - - Ns.int_.<=(3).delete.transact - Ns.int(count).query.get.head ==> 4 - - try { - savepoint { inner => - // Delete all - Ns.int_.delete.transact - Ns.int(count).query.get.head ==> 0 - - // Roll back delete by throwing exception - throw new Exception("inner error...") - } - } catch { - case inner: Exception => - Ns.int(count).query.get.head ==> 4 - // re-throw inner error to outer handler - throw inner - } - } - - Ns.int(count).query.get.head ==> 4 - } - } catch { - case outer: Exception => - outer.getMessage ==> "inner error..." - } - - // Nothing deleted - Ns.int(count).query.get.head ==> 7 - } - - - "rollbackDouble" - types { implicit conn => - Ns.int.insert(1 to 7).transact - - unitOfWork { - savepoint { outer => - Ns.int(count).query.get.head ==> 7 - - Ns.int_.<=(3).delete.transact - Ns.int(count).query.get.head ==> 4 - - savepoint { inner => - // Delete all - Ns.int_.delete.transact - Ns.int(count).query.get.head ==> 0 - - // Roll back outer savepoint delete - outer.rollback() - Ns.int(count).query.get.head ==> 7 - } - - // Nothing deleted - Ns.int(count).query.get.head ==> 7 - } - } - - // Nothing deleted - Ns.int(count).query.get.head ==> 7 - } - } - - - "doubleSavepoint" - { - /* - Only one transaction can be present at a time, but savepoints can be arbitrarily nested. - Uncaught exceptions or explicit `.rollback()` calls would roll back changes done during - the inner savepoint/transaction blocks, while leaving changes applied during outer - savepoint/transaction blocks in-place - */ - "commit" - types { implicit conn => - Ns.int.insert(1 to 7).transact - - unitOfWork { - savepoint { _ => - Ns.int(count).query.get.head ==> 7 - - Ns.int_.<=(2).delete.transact - Ns.int(count).query.get.head ==> 5 - - savepoint { _ => - Ns.int_.<=(4).delete.transact - Ns.int(count).query.get.head ==> 3 - - savepoint { _ => - Ns.int_.<=(6).delete.transact - Ns.int(count).query.get.head ==> 1 - } - Ns.int(count).query.get.head ==> 1 - } - Ns.int(count).query.get.head ==> 1 - } - Ns.int(count).query.get.head ==> 1 - } - - // 1-6 deleted - Ns.int(count).query.get.head ==> 1 - } - - - "throw" - { - - "inner" - types { implicit conn => - Ns.int.insert(1 to 7).transact - - unitOfWork { - savepoint { _ => - Ns.int(count).query.get.head ==> 7 - - Ns.int_.<=(2).delete.transact - Ns.int(count).query.get.head ==> 5 - - savepoint { _ => - Ns.int_.<=(4).delete.transact - Ns.int(count).query.get.head ==> 3 - - try { - savepoint { _ => - Ns.int_.<=(6).delete.transact - Ns.int(count).query.get.head ==> 1 - throw new Exception - } - } catch { - case e: Exception => /*donothing*/ - } - } - Ns.int(count).query.get.head ==> 3 - } - Ns.int(count).query.get.head ==> 3 - } - - // 1-4 deleted - Ns.int(count).query.get.head ==> 3 - } - - - "middle" - types { implicit conn => - Ns.int.insert(1 to 7).transact - - unitOfWork { - savepoint { _ => - Ns.int(count).query.get.head ==> 7 - - Ns.int_.<=(2).delete.transact - Ns.int(count).query.get.head ==> 5 - - try { - savepoint { _ => - Ns.int_.<=(4).delete.transact - Ns.int(count).query.get.head ==> 3 - - savepoint { _ => - Ns.int_.<=(6).delete.transact - Ns.int(count).query.get.head ==> 1 - } - - Ns.int(count).query.get.head ==> 1 - throw new Exception - } - } catch { - case e: Exception => /*donothing*/ - } - } - Ns.int(count).query.get.head ==> 5 - } - - // 1-2 deleted - Ns.int(count).query.get.head ==> 5 - } - - - "innerMiddle" - types { implicit conn => - Ns.int.insert(1 to 7).transact - - unitOfWork { - savepoint { _ => - Ns.int(count).query.get.head ==> 7 - - Ns.int_.<=(2).delete.transact - Ns.int(count).query.get.head ==> 5 - - try { - savepoint { _ => - Ns.int_.<=(4).delete.transact - Ns.int(count).query.get.head ==> 3 - - savepoint { _ => - Ns.int_.<=(6).delete.transact - Ns.int(count).query.get.head ==> 1 - throw new Exception - } - } - } catch { - case e: Exception => /*donothing*/ - } - } - Ns.int(count).query.get.head ==> 5 - } - - // 1-2 deleted - Ns.int(count).query.get.head ==> 5 - } - - - "innerMiddleOuter" - types { implicit conn => - Ns.int.insert(1 to 7).transact - - try { - unitOfWork { - savepoint { _ => - Ns.int(count).query.get.head ==> 7 - - Ns.int_.<=(2).delete.transact - Ns.int(count).query.get.head ==> 5 - - savepoint { _ => - Ns.int_.<=(4).delete.transact - Ns.int(count).query.get.head ==> 3 - - savepoint { _ => - Ns.int_.<=(6).delete.transact - Ns.int(count).query.get.head ==> 1 - throw new Exception - } - } - - Ns.int(count).query.get.head ==> 5 - } - } - } catch { - case e: Exception => /*donothing*/ - } - - // Nothing deleted - Ns.int(count).query.get.head ==> 7 - } - } - } - - - "rollback" - { - - "inner" - types { implicit conn => - Ns.int.insert(1 to 7).transact - - unitOfWork { - savepoint { outer => - Ns.int(count).query.get.head ==> 7 - - Ns.int_.<=(2).delete.transact - Ns.int(count).query.get.head ==> 5 - - savepoint { middle => - Ns.int_.<=(4).delete.transact - Ns.int(count).query.get.head ==> 3 - - savepoint { inner => - Ns.int_.<=(6).delete.transact - Ns.int(count).query.get.head ==> 1 - inner.rollback() - } - } - Ns.int(count).query.get.head ==> 3 - } - Ns.int(count).query.get.head ==> 3 - } - - // 1-4 deleted - Ns.int(count).query.get.head ==> 3 - } - - - "middle" - types { implicit conn => - Ns.int.insert(1 to 7).transact - - unitOfWork { - savepoint { outer => - Ns.int(count).query.get.head ==> 7 - - Ns.int_.<=(2).delete.transact - Ns.int(count).query.get.head ==> 5 - - savepoint { middle => - Ns.int_.<=(4).delete.transact - Ns.int(count).query.get.head ==> 3 - - savepoint { inner => - Ns.int_.<=(6).delete.transact - Ns.int(count).query.get.head ==> 1 - } - - Ns.int(count).query.get.head ==> 1 - middle.rollback() - Ns.int(count).query.get.head ==> 5 - } - } - Ns.int(count).query.get.head ==> 5 - } - - // 1-2 deleted - Ns.int(count).query.get.head ==> 5 - } - - - "innerMiddle" - types { implicit conn => - Ns.int.insert(1 to 7).transact - - unitOfWork { - savepoint { outer => - Ns.int(count).query.get.head ==> 7 - - Ns.int_.<=(2).delete.transact - Ns.int(count).query.get.head ==> 5 - - savepoint { middle => - Ns.int_.<=(4).delete.transact - Ns.int(count).query.get.head ==> 3 - - savepoint { inner => - Ns.int_.<=(6).delete.transact - Ns.int(count).query.get.head ==> 1 - middle.rollback() - Ns.int(count).query.get.head ==> 5 - } - - Ns.int(count).query.get.head ==> 5 - } - } - Ns.int(count).query.get.head ==> 5 - } - - // 1-2 deleted - Ns.int(count).query.get.head ==> 5 - } - - - "middleOuter" - types { implicit conn => - Ns.int.insert(1 to 7).transact - - unitOfWork { - savepoint { outer => - Ns.int(count).query.get.head ==> 7 - - Ns.int_.<=(2).delete.transact - Ns.int(count).query.get.head ==> 5 - - savepoint { middle => - Ns.int_.<=(4).delete.transact - Ns.int(count).query.get.head ==> 3 - - savepoint { inner => - Ns.int_.<=(6).delete.transact - Ns.int(count).query.get.head ==> 1 - } - - Ns.int(count).query.get.head ==> 1 - outer.rollback() - Ns.int(count).query.get.head ==> 7 - } - } - Ns.int(count).query.get.head ==> 7 - } - - // Nothing deleted - Ns.int(count).query.get.head ==> 7 - } - - - "innerMiddleOuter" - types { implicit conn => - Ns.int.insert(1 to 7).transact - - unitOfWork { - savepoint { outer => - Ns.int(count).query.get.head ==> 7 - - Ns.int_.<=(2).delete.transact - Ns.int(count).query.get.head ==> 5 - - savepoint { middle => - Ns.int_.<=(4).delete.transact - Ns.int(count).query.get.head ==> 3 - - savepoint { inner => - Ns.int_.<=(6).delete.transact - Ns.int(count).query.get.head ==> 1 - outer.rollback() - Ns.int(count).query.get.head ==> 7 - } - - Ns.int(count).query.get.head ==> 7 - } - } - Ns.int(count).query.get.head ==> 7 - } - - // Nothing deleted - Ns.int(count).query.get.head ==> 7 - } - } } } diff --git a/sql/sqlite/jvm/src/test/scala/molecule/sql/sqlite/AdhocJVM_sqlite_sync.scala b/sql/sqlite/jvm/src/test/scala/molecule/sql/sqlite/AdhocJVM_sqlite_sync.scala index fac69eff3..f61b6d691 100644 --- a/sql/sqlite/jvm/src/test/scala/molecule/sql/sqlite/AdhocJVM_sqlite_sync.scala +++ b/sql/sqlite/jvm/src/test/scala/molecule/sql/sqlite/AdhocJVM_sqlite_sync.scala @@ -13,7 +13,7 @@ import molecule.sql.sqlite.sync._ //object AdhocJVM_h2 extends TestSuite_h2_array { -object AdhocJVM_sqlite_sync extends TestSuite_sqlite { +object AdhocJVM_sqlite_sync extends TestSuite_sqlite { override lazy val tests = Tests { @@ -45,7 +45,6 @@ object AdhocJVM_sqlite_sync extends TestSuite_sqlite { } - "validation" - validation { implicit conn => import molecule.coreTests.dataModels.dsl.Validation._