Skip to content
Permalink
Browse files

Support transaction isolation levels

Fixes #218. Test in TransactionTest.testTransactions.
  • Loading branch information
szeiger committed Jan 28, 2015
1 parent 6ea7fdb commit a3eec4f970d8f2d391a6a79f30084d167b4039b1
@@ -2,6 +2,8 @@ package com.typesafe.slick.testkit.tests

import com.typesafe.slick.testkit.util.{AsyncTest, JdbcTestDB}

import scala.slick.jdbc.TransactionIsolation

class TransactionTest extends AsyncTest[JdbcTestDB] {
import tdb.profile.api._

@@ -12,6 +14,8 @@ class TransactionTest extends AsyncTest[JdbcTestDB] {
}
val ts = TableQuery[T]

val getTI = SimpleAction(_.session.conn.getTransactionIsolation)

class ExpectedException extends RuntimeException

ts.schema.create andThen { // failed transaction
@@ -57,6 +61,16 @@ class TransactionTest extends AsyncTest[JdbcTestDB] {
} andThen {
ts.to[Set].result.map(_ shouldBe Set(2, 3, 5, 6)) andThen
GetTransactionality.map(_ shouldBe (0, true))
}
} andThen { ifCap(tcap.transactionIsolation) {
(for {
ti1 <- getTI
_ <- (for {
_ <- getTI.map(_ should(_ >= TransactionIsolation.ReadUncommitted.intValue))
_ <- getTI.withTransactionIsolation(TransactionIsolation.Serializable).map(_ should(_ >= TransactionIsolation.Serializable.intValue))
_ <- getTI.map(_ should(_ >= TransactionIsolation.ReadUncommitted.intValue))
} yield ()).withTransactionIsolation(TransactionIsolation.ReadUncommitted)
_ <- getTI.map(_ shouldBe ti1)
} yield ()).withPinnedSession
}}
}
}
@@ -30,8 +30,10 @@ object TestDB {
val jdbcMetaGetFunctions = new Capability("test.jdbcMetaGetFunctions")
/** Supports JDBC metadata getIndexInfo method */
val jdbcMetaGetIndexInfo = new Capability("test.jdbcMetaGetIndexInfo")
/** Supports all tested transaction isolation levels */
val transactionIsolation = new Capability("test.transactionIsolation")

val all = Set(plainSql, jdbcMeta, jdbcMetaGetClientInfoProperties, jdbcMetaGetFunctions, jdbcMetaGetIndexInfo)
val all = Set(plainSql, jdbcMeta, jdbcMetaGetClientInfoProperties, jdbcMetaGetFunctions, jdbcMetaGetIndexInfo, transactionIsolation)
}

/** Copy a file, expanding it if the source name ends with .gz */
@@ -65,6 +65,16 @@ trait JdbcActionComponent extends SqlActionComponent { driver: JdbcDriver =>
def getDumpInfo = DumpInfo(name = "PopStatementParameters")
}

protected class SetTransactionIsolation(ti: Int) extends SynchronousDatabaseAction[Backend, Effect, Int, NoStream] {
def run(ctx: Backend#Context): Int = {
val c = ctx.session.conn
val old = c.getTransactionIsolation
c.setTransactionIsolation(ti)
old
}
def getDumpInfo = DumpInfo(name = "SetTransactionIsolation")
}

class JdbcActionExtensionMethods[E <: Effect, R, S <: NoStream](a: EffectfulAction[E, R, S]) {

/** Run this Action transactionally. This does not guarantee failures to be atomic in the
@@ -79,17 +89,29 @@ trait JdbcActionComponent extends SqlActionComponent { driver: JdbcDriver =>
.asInstanceOf[EffectfulAction[E with Effect.Transactional, R, S]]
)

/** Run this Action with the given statement parameters. Any unset parameter will use the
* current value. The following parameters can be set:
*
* @param rsType The JDBC `ResultSetType`
* @param rsConcurrency The JDBC `ResultSetConcurrency`
* @param rsHoldability The JDBC `ResultSetHoldability`
* @param statementInit A function which is run on every `Statement` or `PreparedStatement`
* directly after creating it. This can be used to set additional
* statement parameters (e.g. `setQueryTimeout`). When multuple
* `withStatementParameters` Actions are nested, all init functions
* are run, starting with the outermost one. */
/** Run this Action with the specified transaction isolation level. This should be used around
* the outermost `transactionally` Action. The semantics of using it inside a transaction are
* database-dependent. It does not create a transaction by itself but it pins the session. */
def withTransactionIsolation(ti: TransactionIsolation): EffectfulAction[E, R, S] = {
val isolated =
(new SetTransactionIsolation(ti.intValue)).flatMap(old => a.andFinally(new SetTransactionIsolation(old)))(Action.sameThreadExecutionContext)
val fused =
if(a.isInstanceOf[SynchronousDatabaseAction[_, _, _, _]]) SynchronousDatabaseAction.fuseUnsafe(isolated)
else isolated
fused.withPinnedSession
}

/** Run this Action with the given statement parameters. Any unset parameter will use the
* current value. The following parameters can be set:
*
* @param rsType The JDBC `ResultSetType`
* @param rsConcurrency The JDBC `ResultSetConcurrency`
* @param rsHoldability The JDBC `ResultSetHoldability`
* @param statementInit A function which is run on every `Statement` or `PreparedStatement`
* directly after creating it. This can be used to set additional
* statement parameters (e.g. `setQueryTimeout`). When multuple
* `withStatementParameters` Actions are nested, all init functions
* are run, starting with the outermost one. */
def withStatementParameters(rsType: ResultSetType = null,
rsConcurrency: ResultSetConcurrency = null,
rsHoldability: ResultSetHoldability = null,
@@ -0,0 +1,34 @@
package scala.slick.jdbc

import java.sql.{Connection, ResultSet}

/** Represents a transaction isolation level. */
sealed abstract class TransactionIsolation(val intValue: Int)

object TransactionIsolation {
/** A transaction isolation level indicating that dirty reads, non-repeatable reads and phantom
* reads can occur. This level allows a row changed by one transaction to be readby another
* transaction before any changes in that row have been committed (a "dirty read"). If any of
* the changes are rolled back, the second transaction will have retrieved an invalid row. */
case object ReadUncommitted extends TransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED)

/** A transaction isolation level indicating that dirty reads are prevented; non-repeatable reads
* and phantom reads can occur. This level only prohibits a transaction from reading a row with
* uncommitted changes in it. */
case object ReadCommitted extends TransactionIsolation(Connection.TRANSACTION_READ_COMMITTED)

/** A transaction isolation level indicating that dirty reads and non-repeatable reads are
* prevented; phantom reads can occur. This level prohibits a transaction from reading a row
* with uncommitted changes in it, and it also prohibits the situation where one transaction
* reads a row, a second transaction alters the row, and the first transaction rereads the row,
* getting different values the second time (a "non-repeatable read"). */
case object RepeatableRead extends TransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ)

/** A transaction isolation level indicating that dirty reads, non-repeatable reads and phantom
* reads are prevented. This level includes the prohibitions in
* <code>TRANSACTION_REPEATABLE_READ</code> and further prohibits the situation where one
* transaction reads all rows that satisfy a <code>WHERE</code> condition, a second transaction
* inserts a row that satisfies that <code>WHERE</code> condition, and the first transaction
* rereads for the same condition, retrieving the additional "phantom" row in the second read. */
case object Serializable extends TransactionIsolation(Connection.TRANSACTION_SERIALIZABLE)
}

0 comments on commit a3eec4f

Please sign in to comment.
You can’t perform that action at this time.