Files
sqlite3/tx.go

216 lines
4.5 KiB
Go
Raw Permalink Normal View History

2023-02-26 03:22:08 +00:00
package sqlite3
import (
"context"
"errors"
"fmt"
2023-03-08 16:29:29 +00:00
"math/rand"
2023-02-26 03:22:08 +00:00
"runtime"
2023-03-08 16:29:29 +00:00
"strconv"
2023-11-07 15:19:40 +00:00
"strings"
2023-02-26 03:22:08 +00:00
)
2023-03-01 13:27:50 +00:00
// Tx is an in-progress database transaction.
2023-03-08 16:29:29 +00:00
//
2023-11-09 16:35:45 +00:00
// https://sqlite.org/lang_transaction.html
2023-02-26 03:22:08 +00:00
type Tx struct {
c *Conn
}
// Begin starts a deferred transaction.
//
2023-11-09 16:35:45 +00:00
// https://sqlite.org/lang_transaction.html
2023-02-26 03:22:08 +00:00
func (c *Conn) Begin() Tx {
2023-03-08 16:29:29 +00:00
// BEGIN even if interrupted.
err := c.txExecInterrupted(`BEGIN DEFERRED`)
if err != nil {
2023-02-26 03:22:08 +00:00
panic(err)
}
return Tx{c}
}
// BeginImmediate starts an immediate transaction.
//
2023-11-09 16:35:45 +00:00
// https://sqlite.org/lang_transaction.html
2023-02-26 03:22:08 +00:00
func (c *Conn) BeginImmediate() (Tx, error) {
err := c.Exec(`BEGIN IMMEDIATE`)
if err != nil {
return Tx{}, err
}
return Tx{c}, nil
}
// BeginExclusive starts an exclusive transaction.
//
2023-11-09 16:35:45 +00:00
// https://sqlite.org/lang_transaction.html
2023-02-26 03:22:08 +00:00
func (c *Conn) BeginExclusive() (Tx, error) {
err := c.Exec(`BEGIN EXCLUSIVE`)
if err != nil {
return Tx{}, err
}
return Tx{c}, nil
}
2023-03-01 10:34:08 +00:00
// End calls either [Tx.Commit] or [Tx.Rollback]
2023-02-26 03:22:08 +00:00
// depending on whether *error points to a nil or non-nil error.
//
// This is meant to be deferred:
//
2023-11-22 13:11:23 +00:00
// func doWork(db *sqlite3.Conn) (err error) {
// tx := db.Begin()
2023-02-26 03:22:08 +00:00
// defer tx.End(&err)
//
// // ... do work in the transaction
// }
//
2023-11-09 16:35:45 +00:00
// https://sqlite.org/lang_transaction.html
2023-02-26 03:22:08 +00:00
func (tx Tx) End(errp *error) {
recovered := recover()
if recovered != nil {
defer panic(recovered)
}
2023-04-21 13:33:24 +01:00
if *errp == nil && recovered == nil {
2023-02-26 03:22:08 +00:00
// Success path.
2023-03-08 16:29:29 +00:00
if tx.c.GetAutocommit() { // There is nothing to commit.
return
}
2023-02-26 03:22:08 +00:00
*errp = tx.Commit()
if *errp == nil {
return
}
2023-03-08 16:29:29 +00:00
// Fall through to the error path.
2023-02-26 03:22:08 +00:00
}
// Error path.
2023-03-08 16:29:29 +00:00
if tx.c.GetAutocommit() { // There is nothing to rollback.
return
}
2023-02-26 03:22:08 +00:00
err := tx.Rollback()
if err != nil {
panic(err)
}
}
2023-03-01 10:34:08 +00:00
// Commit commits the transaction.
//
2023-11-09 16:35:45 +00:00
// https://sqlite.org/lang_transaction.html
2023-02-26 03:22:08 +00:00
func (tx Tx) Commit() error {
return tx.c.Exec(`COMMIT`)
}
2023-03-08 16:29:29 +00:00
// Rollback rolls back the transaction,
// even if the connection has been interrupted.
2023-03-01 10:34:08 +00:00
//
2023-11-09 16:35:45 +00:00
// https://sqlite.org/lang_transaction.html
2023-02-26 03:22:08 +00:00
func (tx Tx) Rollback() error {
2023-03-08 16:29:29 +00:00
return tx.c.txExecInterrupted(`ROLLBACK`)
2023-02-26 03:22:08 +00:00
}
2023-03-08 16:29:29 +00:00
// Savepoint is a marker within a transaction
// that allows for partial rollback.
2023-02-26 03:22:08 +00:00
//
2023-11-09 16:35:45 +00:00
// https://sqlite.org/lang_savepoint.html
2023-03-08 16:29:29 +00:00
type Savepoint struct {
c *Conn
name string
}
// Savepoint establishes a new transaction savepoint.
2023-02-26 03:22:08 +00:00
//
2023-11-09 16:35:45 +00:00
// https://sqlite.org/lang_savepoint.html
2023-03-08 16:29:29 +00:00
func (c *Conn) Savepoint() Savepoint {
// Names can be reused; this makes catching bugs more likely.
2023-11-07 15:19:40 +00:00
name := saveptName() + "_" + strconv.Itoa(int(rand.Int31()))
2023-02-26 03:22:08 +00:00
2023-03-08 16:29:29 +00:00
err := c.txExecInterrupted(fmt.Sprintf("SAVEPOINT %q;", name))
2023-02-26 03:22:08 +00:00
if err != nil {
panic(err)
}
2023-03-08 16:29:29 +00:00
return Savepoint{c: c, name: name}
}
2023-02-26 03:22:08 +00:00
2023-11-07 15:19:40 +00:00
func saveptName() (name string) {
defer func() {
if name == "" {
name = "sqlite3.Savepoint"
}
}()
var pc [8]uintptr
n := runtime.Callers(3, pc[:])
if n <= 0 {
return ""
}
frames := runtime.CallersFrames(pc[:n])
frame, more := frames.Next()
for more && (strings.HasPrefix(frame.Function, "database/sql.") ||
strings.HasPrefix(frame.Function, "github.com/ncruces/go-sqlite3/driver.")) {
frame, more = frames.Next()
}
return frame.Function
}
2023-03-08 16:29:29 +00:00
// Release releases the savepoint rolling back any changes
// if *error points to a non-nil error.
//
// This is meant to be deferred:
//
2023-11-22 13:11:23 +00:00
// func doWork(db *sqlite3.Conn) (err error) {
// savept := db.Savepoint()
2023-03-08 16:29:29 +00:00
// defer savept.Release(&err)
//
// // ... do work in the transaction
// }
func (s Savepoint) Release(errp *error) {
recovered := recover()
if recovered != nil {
defer panic(recovered)
}
2023-02-26 03:22:08 +00:00
2023-04-21 13:33:24 +01:00
if *errp == nil && recovered == nil {
2023-03-08 16:29:29 +00:00
// Success path.
if s.c.GetAutocommit() { // There is nothing to commit.
2023-02-26 03:22:08 +00:00
return
}
2023-03-08 16:29:29 +00:00
*errp = s.c.Exec(fmt.Sprintf("RELEASE %q;", s.name))
if *errp == nil {
return
2023-02-26 03:22:08 +00:00
}
2023-03-08 16:29:29 +00:00
// Fall through to the error path.
}
2023-02-26 03:22:08 +00:00
2023-03-08 16:29:29 +00:00
// Error path.
if s.c.GetAutocommit() { // There is nothing to rollback.
return
}
// ROLLBACK and RELEASE even if interrupted.
err := s.c.txExecInterrupted(fmt.Sprintf(`
ROLLBACK TO %[1]q;
RELEASE %[1]q;
`, s.name))
if err != nil {
panic(err)
}
}
// Rollback rolls the transaction back to the savepoint,
// even if the connection has been interrupted.
// Rollback does not release the savepoint.
//
2023-11-09 16:35:45 +00:00
// https://sqlite.org/lang_transaction.html
2023-03-08 16:29:29 +00:00
func (s Savepoint) Rollback() error {
// ROLLBACK even if interrupted.
return s.c.txExecInterrupted(fmt.Sprintf("ROLLBACK TO %q;", s.name))
}
func (c *Conn) txExecInterrupted(sql string) error {
err := c.Exec(sql)
if errors.Is(err, INTERRUPT) {
2023-02-26 03:22:08 +00:00
old := c.SetInterrupt(context.Background())
defer c.SetInterrupt(old)
2023-03-08 16:29:29 +00:00
err = c.Exec(sql)
2023-02-26 03:22:08 +00:00
}
2023-03-08 16:29:29 +00:00
return err
2023-02-26 03:22:08 +00:00
}