2
0
Files
pgx/tx.go
T
Jack Christensen c53c9e6eb5 Remove simple protocol and one round trip query options
It is impossible to guarantee that the a query executed with the simple
protocol will behave the same as with the extended protocol. This is
because the normal pgx path relies on knowing the OID of query
parameters. Without this encoding a value can only be determined by the
value instead of the combination of value and PostgreSQL type. For
example, how should a []int32 be encoded? It might be encoded into a
PostgreSQL int4[] or json.

Removal also simplifies the core query path.

The primary reason for the simple protocol is for servers like PgBouncer
that may not be able to support normal prepared statements. After
further research it appears that issuing a "flush" instead "sync" after
preparing the unnamed statement would allow PgBouncer to work.

The one round trip mode can be better handled with prepared statements.

As a last resort, all original server functionality can still be accessed by
dropping down to PgConn.
2019-04-13 11:39:01 -05:00

213 lines
5.8 KiB
Go

package pgx
import (
"bytes"
"context"
"fmt"
"github.com/jackc/pgconn"
"github.com/pkg/errors"
)
type TxIsoLevel string
// Transaction isolation levels
const (
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 (
TxStatusInProgress = 0
TxStatusCommitFailure = -1
TxStatusRollbackFailure = -2
TxStatusInFailure = -3
TxStatusCommitSuccess = 1
TxStatusRollbackSuccess = 2
)
type TxOptions struct {
IsoLevel TxIsoLevel
AccessMode TxAccessMode
DeferrableMode TxDeferrableMode
}
func (txOptions *TxOptions) beginSQL() string {
if txOptions == nil {
return "begin"
}
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)
}
return buf.String()
}
var ErrTxClosed = errors.New("tx is closed")
var ErrTxInFailure = errors.New("tx failed")
// ErrTxCommitRollback occurs when an error has occurred in a transaction and
// Commit() is called. PostgreSQL accepts COMMIT on aborted transactions, but
// it is treated as ROLLBACK.
var ErrTxCommitRollback = errors.New("commit unexpectedly resulted in rollback")
// 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.BeginEx(context.Background(), nil)
}
// BeginEx starts a transaction with txOptions determining the transaction
// mode. Unlike database/sql, the context only affects the begin command. i.e.
// there is no auto-rollback on context cancelation.
func (c *Conn) BeginEx(ctx context.Context, txOptions *TxOptions) (*Tx, error) {
_, err := c.Exec(ctx, txOptions.beginSQL())
if err != nil {
// begin should never fail unless there is an underlying connection issue or
// a context timeout. In either case, the connection is possibly broken.
c.die(errors.New("failed to begin transaction"))
return nil, err
}
return &Tx{conn: c}, nil
}
// Tx represents a database transaction.
//
// All Tx methods return ErrTxClosed if Commit or Rollback has already been
// called on the Tx.
type Tx struct {
conn *Conn
err error
status int8
}
// Commit commits the transaction.
func (tx *Tx) Commit(ctx context.Context) error {
if tx.status != TxStatusInProgress {
return ErrTxClosed
}
commandTag, err := tx.conn.Exec(ctx, "commit")
if err == nil && string(commandTag) == "COMMIT" {
tx.status = TxStatusCommitSuccess
} else if err == nil && string(commandTag) == "ROLLBACK" {
tx.status = TxStatusCommitFailure
tx.err = ErrTxCommitRollback
} else {
tx.status = TxStatusCommitFailure
tx.err = err
// A commit failure leaves the connection in an undefined state
tx.conn.die(errors.New("commit failed"))
}
return tx.err
}
// Rollback rolls back the transaction. Rollback will return ErrTxClosed if the
// Tx is already closed, but is otherwise safe to call multiple times. Hence, a
// defer tx.Rollback() is safe even if tx.Commit() will be called first in a
// non-error condition.
func (tx *Tx) Rollback(ctx context.Context) error {
if tx.status != TxStatusInProgress {
return ErrTxClosed
}
_, tx.err = tx.conn.Exec(ctx, "rollback")
if tx.err == nil {
tx.status = TxStatusRollbackSuccess
} else {
tx.status = TxStatusRollbackFailure
// A rollback failure leaves the connection in an undefined state
tx.conn.die(errors.New("rollback failed"))
}
return tx.err
}
// Exec delegates to the underlying *Conn
func (tx *Tx) Exec(ctx context.Context, sql string, arguments ...interface{}) (commandTag pgconn.CommandTag, err error) {
return tx.conn.Exec(ctx, sql, arguments...)
}
// Prepare delegates to the underlying *Conn
func (tx *Tx) Prepare(name, sql string) (*PreparedStatement, error) {
return tx.PrepareEx(context.Background(), name, sql, nil)
}
// PrepareEx delegates to the underlying *Conn
func (tx *Tx) PrepareEx(ctx context.Context, name, sql string, opts *PrepareExOptions) (*PreparedStatement, error) {
if tx.status != TxStatusInProgress {
return nil, ErrTxClosed
}
return tx.conn.PrepareEx(ctx, name, sql, opts)
}
// Query delegates to the underlying *Conn
func (tx *Tx) Query(ctx context.Context, sql string, args ...interface{}) (Rows, error) {
if tx.status != TxStatusInProgress {
// Because checking for errors can be deferred to the *Rows, build one with the error
err := ErrTxClosed
return &connRows{closed: true, err: err}, err
}
return tx.conn.Query(ctx, sql, args...)
}
// QueryRow delegates to the underlying *Conn
func (tx *Tx) QueryRow(ctx context.Context, sql string, args ...interface{}) Row {
rows, _ := tx.Query(ctx, sql, args...)
return (*connRow)(rows.(*connRows))
}
// CopyFrom delegates to the underlying *Conn
func (tx *Tx) CopyFrom(tableName Identifier, columnNames []string, rowSrc CopyFromSource) (int, error) {
if tx.status != TxStatusInProgress {
return 0, ErrTxClosed
}
return tx.conn.CopyFrom(tableName, columnNames, rowSrc)
}
// Status returns the status of the transaction from the set of
// pgx.TxStatus* constants.
func (tx *Tx) Status() int8 {
if tx.status == TxStatusInProgress && tx.conn.pgConn.TxStatus == 'E' {
return TxStatusInFailure
}
return tx.status
}
// Err returns the final error state, if any, of calling Commit or Rollback.
func (tx *Tx) Err() error {
return tx.err
}