Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dbkit/sqlite/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/gopherium/framework/dbkit/sqlite
go 1.27.2

require (
github.com/gopherium/framework/dbkit v0.1.0
github.com/gopherium/framework/dbkit v0.3.0
github.com/pressly/goose/v3 v3.27.2
golang.org/x/sys v0.48.0
modernc.org/sqlite v1.60.1
Expand Down
4 changes: 2 additions & 2 deletions dbkit/sqlite/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFe
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherium/framework/dbkit v0.1.0 h1:jeFld4uZu555PkgbZQRKkDK1WOOB8am+Wu2Ar4xX9Jw=
github.com/gopherium/framework/dbkit v0.1.0/go.mod h1:M837GTBK4MLEt0oqvEKhCQydKFNhVLWvsDlAcqT21co=
github.com/gopherium/framework/dbkit v0.3.0 h1:EkMU4dg6YkiIrn3SJH0oJ2KzLNIXrDMu4K+HHoraEIs=
github.com/gopherium/framework/dbkit v0.3.0/go.mod h1:sGsNkpbHe1LRW7k1qEjTzqbeHVYOnoCI5e1l/81TAvQ=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
Expand Down
31 changes: 31 additions & 0 deletions dbkit/sqlite/intx_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: Apache-2.0

package sqlite_test

import (
"database/sql"
"errors"
"testing"

"github.com/gopherium/framework/dbkit"
"github.com/gopherium/framework/dbkit/sqlite/sqlitetest"
)

func TestInTxJoinsBodyAndRollbackErrors(t *testing.T) {
t.Parallel()

errBody := errors.New("sqlite test: the transaction body failed on purpose")
errRollback := errors.New("sqlite test: the rollback failed on purpose")
faults := &sqlitetest.Faults{}
db := sqlitetest.OpenWithFaults(t, testOptions(), faults)
faults.FailNextRollback(errRollback)

err := dbkit.InTx(t.Context(), db, func(*sql.Tx) error { return errBody })

if !errors.Is(err, errBody) {
t.Errorf("InTx() error = %v, want it to match the body error", err)
}
if !errors.Is(err, errRollback) {
t.Errorf("InTx() error = %v, want it to match the rollback error", err)
}
}
34 changes: 30 additions & 4 deletions dbkit/sqlite/sqlitetest/faults.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import (

// Faults holds the failures the connections of a handle from OpenWithFaults answer with.
type Faults struct {
// mu guards statements and commit.
// mu guards statements, commit and rollback.
mu sync.Mutex
// statements maps the exact text of each chosen statement to the error it answers with.
statements map[string]error
// commit is the error the next commit answers with, or nil.
commit error
// rollback is the error the next rollback answers with, or nil.
rollback error
}

// FailStatement makes every statement whose text is exactly query answer err, inside transactions too.
Expand Down Expand Up @@ -59,6 +61,22 @@ func (f *Faults) takeCommit() error {
return err
}

// FailNextRollback makes the next rollback end its transaction and answer err.
func (f *Faults) FailNextRollback(err error) {
f.mu.Lock()
defer f.mu.Unlock()
f.rollback = err
}

// takeRollback returns the error the next rollback answers with and clears it.
func (f *Faults) takeRollback() error {
f.mu.Lock()
defer f.mu.Unlock()
err := f.rollback
f.rollback = nil
return err
}

// wrap returns c answering with the failures of f.
func (f *Faults) wrap(c driver.Conn) driver.Conn {
return &faultConn{driverConn: c.(driverConn), faults: f}
Expand Down Expand Up @@ -169,12 +187,12 @@ func (s *faultStmt) QueryContext(ctx context.Context, args []driver.NamedValue)
return s.driverStmt.QueryContext(ctx, args)
}

// Begin starts a transaction whose commit answers with the next commit failure.
// Begin starts a transaction whose commit and rollback answer with the next matching failure.
func (c *faultConn) Begin() (driver.Tx, error) {
return c.BeginTx(context.Background(), driver.TxOptions{})
}

// BeginTx starts a transaction whose commit answers with the next commit failure.
// BeginTx starts a transaction whose commit and rollback answer with the next matching failure.
func (c *faultConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
tx, err := c.driverConn.BeginTx(ctx, opts)
if err != nil {
Expand All @@ -183,7 +201,7 @@ func (c *faultConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.
return &faultTx{Tx: tx, faults: c.faults}, nil
}

// faultTx is a driver transaction whose commit answers with the next commit failure.
// faultTx is a driver transaction whose commit and rollback answer with the next matching failure.
type faultTx struct {
// Tx is the driver's own transaction.
driver.Tx
Expand All @@ -198,3 +216,11 @@ func (t *faultTx) Commit() error {
}
return t.Tx.Commit()
}

// Rollback ends the transaction and answers the next rollback failure, or the driver's answer when none is set.
func (t *faultTx) Rollback() error {
if err := t.faults.takeRollback(); err != nil {
return errors.Join(err, t.Tx.Rollback())
}
return t.Tx.Rollback()
}
23 changes: 23 additions & 0 deletions dbkit/sqlite/sqlitetest/faults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,29 @@ func TestFaultFailsTheChosenCommit(t *testing.T) {
mustCount(t, db, 1)
}

func TestFaultFailsTheChosenRollback(t *testing.T) {
t.Parallel()

faults := &sqlitetest.Faults{}
db := faultOpen(t, faults)
faults.FailNextRollback(errInjected)
tx := mustBegin(t, db)
mustExec(t, tx, faultInsert)

err := tx.Rollback()

if !errors.Is(err, errInjected) {
t.Errorf("Rollback() error = %v, want the injected error", err)
}
mustCount(t, db, 0)
next := mustBegin(t, db)
mustExec(t, next, "INSERT INTO fault_rows VALUES (2)")
if err := next.Rollback(); err != nil {
t.Errorf("the next Rollback() error = %v, want nil once the fault answered", err)
}
mustCount(t, db, 0)
}

func TestFaultFailsTheChosenStatementInsideATransaction(t *testing.T) {
t.Parallel()

Expand Down
Loading