diff --git a/conn_pool.go b/conn_pool.go index 67868769..126d5b14 100644 --- a/conn_pool.go +++ b/conn_pool.go @@ -383,7 +383,7 @@ func (p *ConnPool) QueryRow(sql string, args ...interface{}) *Row { // Begin acquires a connection and begins a transaction on it. When the // transaction is closed the connection will be automatically released. func (p *ConnPool) Begin() (*Tx, error) { - return p.BeginIso("") + return p.BeginEx(nil) } // Prepare creates a prepared statement on a connection in the pool to test the @@ -469,17 +469,17 @@ func (p *ConnPool) Deallocate(name string) (err error) { return nil } -// BeginIso acquires a connection and begins a transaction in isolation mode iso -// on it. When the transaction is closed the connection will be automatically -// released. -func (p *ConnPool) BeginIso(iso string) (*Tx, error) { +// BeginEx acquires a connection and starts a transaction with txOptions +// determining the transaction mode. When the transaction is closed the +// connection will be automatically released. +func (p *ConnPool) BeginEx(txOptions *TxOptions) (*Tx, error) { for { c, err := p.Acquire() if err != nil { return nil, err } - tx, err := c.BeginIso(iso) + tx, err := c.BeginEx(txOptions) if err != nil { alive := c.IsAlive() p.Release(c) diff --git a/conn_pool_test.go b/conn_pool_test.go index db8702fb..212a79d9 100644 --- a/conn_pool_test.go +++ b/conn_pool_test.go @@ -560,9 +560,9 @@ func TestConnPoolTransactionIso(t *testing.T) { pool := createConnPool(t, 2) defer pool.Close() - tx, err := pool.BeginIso(pgx.Serializable) + tx, err := pool.BeginEx(&pgx.TxOptions{IsoLevel: pgx.Serializable}) if err != nil { - t.Fatalf("pool.Begin failed: %v", err) + t.Fatalf("pool.BeginEx failed: %v", err) } defer tx.Rollback() diff --git a/tx.go b/tx.go index 36f99c28..a636b364 100644 --- a/tx.go +++ b/tx.go @@ -1,16 +1,35 @@ package pgx import ( + "bytes" "errors" "fmt" ) +type TxIsoLevel string + // Transaction isolation levels const ( - Serializable = "serializable" - RepeatableRead = "repeatable read" - ReadCommitted = "read committed" - ReadUncommitted = "read uncommitted" + Serializable = TxIsoLevel("serializable") + RepeatableRead = TxIsoLevel("repeatable read") + ReadCommitted = TxIsoLevel("read committed") + ReadUncommitted = TxIsoLevel("read uncommitted") +) + +type TxAccessMode string + +// Transaction access modes +const ( + ReadWrite = TxAccessMode("read write") + ReadOnly = TxAccessMode("read only") +) + +type TxDeferrableMode string + +// Transaction deferrable modes +const ( + Deferrable = TxDeferrableMode("deferrable") + NotDeferrable = TxDeferrableMode("not deferrable") ) const ( @@ -21,6 +40,12 @@ const ( TxStatusRollbackSuccess = 2 ) +type TxOptions struct { + IsoLevel TxIsoLevel + AccessMode TxAccessMode + DeferrableMode TxDeferrableMode +} + var ErrTxClosed = errors.New("tx is closed") // ErrTxCommitRollback occurs when an error has occurred in a transaction and @@ -28,30 +53,32 @@ var ErrTxClosed = errors.New("tx is closed") // it is treated as ROLLBACK. var ErrTxCommitRollback = errors.New("commit unexpectedly resulted in rollback") -// Begin starts a transaction with the default isolation level for the current -// connection. To use a specific isolation level see BeginIso. +// Begin starts a transaction with the default transaction mode for the +// current connection. To use a specific transaction mode see BeginEx. func (c *Conn) Begin() (*Tx, error) { - return c.begin("") + return c.BeginEx(nil) } -// BeginIso starts a transaction with isoLevel as the transaction isolation -// level. -// -// Valid isolation levels (and their constants) are: -// serializable (pgx.Serializable) -// repeatable read (pgx.RepeatableRead) -// read committed (pgx.ReadCommitted) -// read uncommitted (pgx.ReadUncommitted) -func (c *Conn) BeginIso(isoLevel string) (*Tx, error) { - return c.begin(isoLevel) -} - -func (c *Conn) begin(isoLevel string) (*Tx, error) { +// BeginEx starts a transaction with txOptions determining the transaction +// mode. +func (c *Conn) BeginEx(txOptions *TxOptions) (*Tx, error) { var beginSQL string - if isoLevel == "" { + if txOptions == nil { beginSQL = "begin" } else { - beginSQL = fmt.Sprintf("begin isolation level %s", isoLevel) + buf := &bytes.Buffer{} + buf.WriteString("begin") + if txOptions.IsoLevel != "" { + fmt.Fprintf(buf, " isolation level %s", txOptions.IsoLevel) + } + if txOptions.AccessMode != "" { + fmt.Fprintf(buf, " %s", txOptions.AccessMode) + } + if txOptions.DeferrableMode != "" { + fmt.Fprintf(buf, " %s", txOptions.DeferrableMode) + } + + beginSQL = buf.String() } _, err := c.Exec(beginSQL) diff --git a/tx_test.go b/tx_test.go index 435521a3..0ba5904b 100644 --- a/tx_test.go +++ b/tx_test.go @@ -107,15 +107,15 @@ func TestTxCommitSerializationFailure(t *testing.T) { } defer pool.Exec(`drop table tx_serializable_sums`) - tx1, err := pool.BeginIso(pgx.Serializable) + tx1, err := pool.BeginEx(&pgx.TxOptions{IsoLevel: pgx.Serializable}) if err != nil { - t.Fatalf("BeginIso failed: %v", err) + t.Fatalf("BeginEx failed: %v", err) } defer tx1.Rollback() - tx2, err := pool.BeginIso(pgx.Serializable) + tx2, err := pool.BeginEx(&pgx.TxOptions{IsoLevel: pgx.Serializable}) if err != nil { - t.Fatalf("BeginIso failed: %v", err) + t.Fatalf("BeginEx failed: %v", err) } defer tx2.Rollback() @@ -182,20 +182,20 @@ func TestTransactionSuccessfulRollback(t *testing.T) { } } -func TestBeginIso(t *testing.T) { +func TestBeginExIsoLevels(t *testing.T) { t.Parallel() conn := mustConnect(t, *defaultConnConfig) defer closeConn(t, conn) - isoLevels := []string{pgx.Serializable, pgx.RepeatableRead, pgx.ReadCommitted, pgx.ReadUncommitted} + isoLevels := []pgx.TxIsoLevel{pgx.Serializable, pgx.RepeatableRead, pgx.ReadCommitted, pgx.ReadUncommitted} for _, iso := range isoLevels { - tx, err := conn.BeginIso(iso) + tx, err := conn.BeginEx(&pgx.TxOptions{IsoLevel: iso}) if err != nil { - t.Fatalf("conn.BeginIso failed: %v", err) + t.Fatalf("conn.BeginEx failed: %v", err) } - var level string + var level pgx.TxIsoLevel conn.QueryRow("select current_setting('transaction_isolation')").Scan(&level) if level != iso { t.Errorf("Expected to be in isolation level %v but was %v", iso, level) @@ -208,6 +208,24 @@ func TestBeginIso(t *testing.T) { } } +func TestBeginExReadOnly(t *testing.T) { + t.Parallel() + + conn := mustConnect(t, *defaultConnConfig) + defer closeConn(t, conn) + + tx, err := conn.BeginEx(&pgx.TxOptions{AccessMode: pgx.ReadOnly}) + if err != nil { + t.Fatalf("conn.BeginEx failed: %v", err) + } + defer tx.Rollback() + + _, err = conn.Exec("create table foo(id serial primary key)") + if pgErr, ok := err.(pgx.PgError); !ok || pgErr.Code != "25006" { + t.Errorf("Expected error SQLSTATE 25006, but got %#v", err) + } +} + func TestTxAfterClose(t *testing.T) { t.Parallel() diff --git a/v3.md b/v3.md index 4a80ab11..c4f04d7d 100644 --- a/v3.md +++ b/v3.md @@ -12,6 +12,10 @@ Rename Uuid to UUID in accordance with Go naming conventions. Logger interface reduced to single Log method. +Replace BeginIso with BeginEx. BeginEx adds support for read/write mode and deferrable mode. + +Transaction isolation level constants are now typed strings instead of bare strings. + ## TODO / Possible / Investigate Organize errors better