From b7ddbfbd2aa6c5c814d3acd46f8a71c4df5225db Mon Sep 17 00:00:00 2001 From: Thibaut Rousseau Date: Tue, 28 Oct 2025 18:53:01 +0100 Subject: [PATCH] allow to isolate distinct transactor instances --- pgx/transactor.go | 42 ++++------ pgx/types.go | 12 +-- sqlx/transactor.go | 18 +++- sqlx/types.go | 12 +-- stdlib/fake_transactor.go | 10 +-- stdlib/transactor.go | 28 ++++--- stdlib/types.go | 12 +-- tests/go.mod | 1 + tests/go.sum | 2 + tests/pgx/transactor_test.go | 142 +++++++++++++++++++++++++++++++- tests/sqlx/transactor_test.go | 46 ++++++++++- tests/stdlib/transactor_test.go | 44 +++++++++- transactor.go | 4 + 13 files changed, 310 insertions(+), 63 deletions(-) diff --git a/pgx/transactor.go b/pgx/transactor.go index 937804a..be33ad0 100644 --- a/pgx/transactor.go +++ b/pgx/transactor.go @@ -4,13 +4,14 @@ import ( "context" "fmt" - "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) -func NewTransactor(db *pgx.Conn) (*Transactor, DBGetter) { +func NewTransactor(db pgxDB) (*Transactor, DBGetter) { + txKey := &transactorKey{} + pgxTxGetter := func(ctx context.Context) pgxDB { - if tx := txFromContext(ctx); tx != nil { + if tx := txFromContext(ctx, txKey); tx != nil { return tx } @@ -18,7 +19,7 @@ func NewTransactor(db *pgx.Conn) (*Transactor, DBGetter) { } dbGetter := func(ctx context.Context) DB { - if tx := txFromContext(ctx); tx != nil { + if tx := txFromContext(ctx, txKey); tx != nil { return tx } @@ -27,29 +28,13 @@ func NewTransactor(db *pgx.Conn) (*Transactor, DBGetter) { return &Transactor{ pgxTxGetter, + txKey, }, dbGetter } +// Deprecated: use [NewTransactor] instead. func NewTransactorFromPool(pool *pgxpool.Pool) (*Transactor, DBGetter) { - pgxTxGetter := func(ctx context.Context) pgxDB { - if tx := txFromContext(ctx); tx != nil { - return tx - } - - return pool - } - - dbGetter := func(ctx context.Context) DB { - if tx := txFromContext(ctx); tx != nil { - return tx - } - - return pool - } - - return &Transactor{ - pgxTxGetter, - }, dbGetter + return NewTransactor(pool) } type ( @@ -58,6 +43,7 @@ type ( type Transactor struct { pgxTxGetter + txKey *transactorKey } func (t *Transactor) WithinTransaction(ctx context.Context, txFunc func(context.Context) error) error { @@ -71,7 +57,7 @@ func (t *Transactor) WithinTransaction(ctx context.Context, txFunc func(context. _ = tx.Rollback(ctx) // If rollback fails, there's nothing to do, the transaction will expire by itself }() - txCtx := txToContext(ctx, tx) + txCtx := txToContext(ctx, t.txKey, tx) if err := txFunc(txCtx); err != nil { return err @@ -84,6 +70,12 @@ func (t *Transactor) WithinTransaction(ctx context.Context, txFunc func(context. return nil } +func (t *Transactor) IsWithinTransaction(ctx context.Context) bool { + return ctx.Value(t.txKey) != nil +} + +// Deprecated: use [Transactor.IsWithinTransaction] instead. +// This function can give the wrong result if multiple transactor instances are used. func IsWithinTransaction(ctx context.Context) bool { - return ctx.Value(transactorKey{}) != nil + return ctx.Value(transactorMarker{}) != nil } diff --git a/pgx/types.go b/pgx/types.go index e199684..ec3cf8c 100644 --- a/pgx/types.go +++ b/pgx/types.go @@ -37,18 +37,20 @@ var ( ) type ( - transactorKey struct{} + transactorKey struct{ _ *struct{} } + // Deprecated: transactorMarker is used in addition to transactorKey to keep the legacy IsWithinTransaction function. + transactorMarker struct{} // DBGetter is used to get the current DB handler from the context. // It returns the current transaction if there is one, otherwise it will return the original DB. DBGetter func(context.Context) DB ) -func txToContext(ctx context.Context, tx pgx.Tx) context.Context { - return context.WithValue(ctx, transactorKey{}, tx) +func txToContext(ctx context.Context, key *transactorKey, tx pgx.Tx) context.Context { + return context.WithValue(context.WithValue(ctx, key, tx), transactorMarker{}, struct{}{}) } -func txFromContext(ctx context.Context) pgx.Tx { - if tx, ok := ctx.Value(transactorKey{}).(pgx.Tx); ok { +func txFromContext(ctx context.Context, key *transactorKey) pgx.Tx { + if tx, ok := ctx.Value(key).(pgx.Tx); ok { return tx } diff --git a/sqlx/transactor.go b/sqlx/transactor.go index 9621b56..9bf0906 100644 --- a/sqlx/transactor.go +++ b/sqlx/transactor.go @@ -8,8 +8,10 @@ import ( ) func NewTransactor(db *sqlx.DB, nestedTransactionStrategy nestedTransactionsStrategy) (*Transactor, DBGetter) { + txKey := &transactorKey{} + sqlDBGetter := func(ctx context.Context) sqlxDB { - if tx := txFromContext(ctx); tx != nil { + if tx := txFromContext(ctx, txKey); tx != nil { return tx } @@ -17,7 +19,7 @@ func NewTransactor(db *sqlx.DB, nestedTransactionStrategy nestedTransactionsStra } dbGetter := func(ctx context.Context) DB { - if tx := txFromContext(ctx); tx != nil { + if tx := txFromContext(ctx, txKey); tx != nil { return tx } @@ -27,6 +29,7 @@ func NewTransactor(db *sqlx.DB, nestedTransactionStrategy nestedTransactionsStra return &Transactor{ sqlDBGetter, nestedTransactionStrategy, + txKey, }, dbGetter } @@ -38,6 +41,7 @@ type ( type Transactor struct { sqlxDBGetter nestedTransactionsStrategy + txKey *transactorKey } func (t *Transactor) WithinTransaction(ctx context.Context, txFunc func(context.Context) error) error { @@ -52,7 +56,7 @@ func (t *Transactor) WithinTransaction(ctx context.Context, txFunc func(context. defer func() { _ = currentTX.Rollback() // If rollback fails, there's nothing to do, the transaction will expire by itself }() - txCtx := txToContext(ctx, newDB) + txCtx := txToContext(ctx, t.txKey, newDB) if err := txFunc(txCtx); err != nil { return err @@ -65,6 +69,12 @@ func (t *Transactor) WithinTransaction(ctx context.Context, txFunc func(context. return nil } +func (t *Transactor) IsWithinTransaction(ctx context.Context) bool { + return ctx.Value(t.txKey) != nil +} + +// Deprecated: use [Transactor.IsWithinTransaction] instead. +// This function can give the wrong result if multiple transactor instances are used. func IsWithinTransaction(ctx context.Context) bool { - return ctx.Value(transactorKey{}) != nil + return ctx.Value(transactorMarker{}) != nil } diff --git a/sqlx/types.go b/sqlx/types.go index 8f97713..f09531c 100644 --- a/sqlx/types.go +++ b/sqlx/types.go @@ -65,18 +65,20 @@ var ( ) type ( - transactorKey struct{} + transactorKey struct{ _ *struct{} } + // Deprecated: transactorMarker is used in addition to transactorKey to keep the legacy IsWithinTransaction function. + transactorMarker struct{} // DBGetter is used to get the current DB handler from the context. // It returns the current transaction if there is one, otherwise it will return the original DB. DBGetter func(context.Context) DB ) -func txToContext(ctx context.Context, tx sqlxDB) context.Context { - return context.WithValue(ctx, transactorKey{}, tx) +func txToContext(ctx context.Context, key *transactorKey, tx sqlxDB) context.Context { + return context.WithValue(context.WithValue(ctx, key, tx), transactorMarker{}, struct{}{}) } -func txFromContext(ctx context.Context) sqlxDB { - if tx, ok := ctx.Value(transactorKey{}).(sqlxDB); ok { +func txFromContext(ctx context.Context, key *transactorKey) sqlxDB { + if tx, ok := ctx.Value(key).(sqlxDB); ok { return tx } diff --git a/stdlib/fake_transactor.go b/stdlib/fake_transactor.go index b50ca53..61fea36 100644 --- a/stdlib/fake_transactor.go +++ b/stdlib/fake_transactor.go @@ -3,22 +3,20 @@ package stdlib import ( "context" "database/sql" - - "github.com/Thiht/transactor" ) // NewFakeTransactor initializes a Transactor and DBGetter that do nothing: // - the Transactor just executes its callback and returns the error, // - the DBGetter just returns the DB handler. // They can be used in tests where the transaction system itself doesn't need to be tested. -func NewFakeTransactor(db *sql.DB) (transactor.Transactor, DBGetter) { - return fakeTransactor{}, func(_ context.Context) DB { +func NewFakeTransactor(db *sql.DB) (FakeTransactor, DBGetter) { + return FakeTransactor{}, func(_ context.Context) DB { return db } } -type fakeTransactor struct{} +type FakeTransactor struct{} -func (fakeTransactor) WithinTransaction(ctx context.Context, txFunc func(context.Context) error) error { +func (FakeTransactor) WithinTransaction(ctx context.Context, txFunc func(context.Context) error) error { return txFunc(ctx) } diff --git a/stdlib/transactor.go b/stdlib/transactor.go index 0d11ee8..de4a704 100644 --- a/stdlib/transactor.go +++ b/stdlib/transactor.go @@ -4,13 +4,13 @@ import ( "context" "database/sql" "fmt" - - "github.com/Thiht/transactor" ) -func NewTransactor(db *sql.DB, nestedTransactionStrategy nestedTransactionsStrategy) (transactor.Transactor, DBGetter) { +func NewTransactor(db *sql.DB, nestedTransactionStrategy nestedTransactionsStrategy) (*Transactor, DBGetter) { + txKey := &transactorKey{} + sqlDBGetter := func(ctx context.Context) sqlDB { - if tx := txFromContext(ctx); tx != nil { + if tx := txFromContext(ctx, txKey); tx != nil { return tx } @@ -18,16 +18,17 @@ func NewTransactor(db *sql.DB, nestedTransactionStrategy nestedTransactionsStrat } dbGetter := func(ctx context.Context) DB { - if tx := txFromContext(ctx); tx != nil { + if tx := txFromContext(ctx, txKey); tx != nil { return tx } return db } - return &stdlibTransactor{ + return &Transactor{ sqlDBGetter, nestedTransactionStrategy, + txKey, }, dbGetter } @@ -36,12 +37,13 @@ type ( nestedTransactionsStrategy func(sqlDB, *sql.Tx) (sqlDB, sqlTx) ) -type stdlibTransactor struct { +type Transactor struct { sqlDBGetter nestedTransactionsStrategy + txKey *transactorKey } -func (t *stdlibTransactor) WithinTransaction(ctx context.Context, txFunc func(context.Context) error) error { +func (t *Transactor) WithinTransaction(ctx context.Context, txFunc func(context.Context) error) error { currentDB := t.sqlDBGetter(ctx) tx, err := currentDB.BeginTx(ctx, nil) @@ -53,7 +55,7 @@ func (t *stdlibTransactor) WithinTransaction(ctx context.Context, txFunc func(co defer func() { _ = currentTX.Rollback() // If rollback fails, there's nothing to do, the transaction will expire by itself }() - txCtx := txToContext(ctx, newDB) + txCtx := txToContext(ctx, t.txKey, newDB) if err := txFunc(txCtx); err != nil { return err @@ -66,6 +68,12 @@ func (t *stdlibTransactor) WithinTransaction(ctx context.Context, txFunc func(co return nil } +func (t *Transactor) IsWithinTransaction(ctx context.Context) bool { + return ctx.Value(t.txKey) != nil +} + +// Deprecated: use [Transactor.IsWithinTransaction] instead. +// This function can give the wrong result if multiple transactor instances are used. func IsWithinTransaction(ctx context.Context) bool { - return ctx.Value(transactorKey{}) != nil + return ctx.Value(transactorMarker{}) != nil } diff --git a/stdlib/types.go b/stdlib/types.go index d31ce89..cc20974 100644 --- a/stdlib/types.go +++ b/stdlib/types.go @@ -36,18 +36,20 @@ var ( ) type ( - transactorKey struct{} + transactorKey struct{ _ *struct{} } + // Deprecated: transactorMarker is used in addition to transactorKey to keep the legacy IsWithinTransaction function. + transactorMarker struct{} // DBGetter is used to get the current DB handler from the context. // It returns the current transaction if there is one, otherwise it will return the original DB. DBGetter func(context.Context) DB ) -func txToContext(ctx context.Context, tx sqlDB) context.Context { - return context.WithValue(ctx, transactorKey{}, tx) +func txToContext(ctx context.Context, key *transactorKey, tx sqlDB) context.Context { + return context.WithValue(context.WithValue(ctx, key, tx), transactorMarker{}, struct{}{}) } -func txFromContext(ctx context.Context) sqlDB { - if tx, ok := ctx.Value(transactorKey{}).(sqlDB); ok { +func txFromContext(ctx context.Context, key *transactorKey) sqlDB { + if tx, ok := ctx.Value(key).(sqlDB); ok { return tx } diff --git a/tests/go.mod b/tests/go.mod index 414f565..a9419b2 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -11,6 +11,7 @@ require ( github.com/jackc/pgx/v5 v5.7.6 github.com/jmoiron/sqlx v1.4.0 github.com/microsoft/go-mssqldb v1.9.3 + github.com/pashagolub/pgxmock/v4 v4.9.0 github.com/sijms/go-ora/v2 v2.9.0 github.com/stretchr/testify v1.11.1 github.com/testcontainers/testcontainers-go v0.39.0 diff --git a/tests/go.sum b/tests/go.sum index 63ab42b..b4e44e0 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -138,6 +138,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pashagolub/pgxmock/v4 v4.9.0 h1:itlO8nrVRnzkdMBXLs8pWUyyB2PC3Gku0WGIj/gGl7I= +github.com/pashagolub/pgxmock/v4 v4.9.0/go.mod h1:9L57pC193h2aKRHVyiiE817avasIPZnPwPlw3JczWvM= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= diff --git a/tests/pgx/transactor_test.go b/tests/pgx/transactor_test.go index f5c9f35..ac857d5 100644 --- a/tests/pgx/transactor_test.go +++ b/tests/pgx/transactor_test.go @@ -2,19 +2,159 @@ package pgx_test import ( "context" + "errors" "testing" + "github.com/Thiht/transactor" pgxTransactor "github.com/Thiht/transactor/pgx" + "github.com/pashagolub/pgxmock/v4" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestIsWithinTransaction(t *testing.T) { +func TestTransactor(t *testing.T) { + t.Parallel() + + t.Run("it should implement the Transactor interface", func(t *testing.T) { + t.Parallel() + assert.Implements(t, (*transactor.Transactor)(nil), &pgxTransactor.Transactor{}) + }) + + t.Run("it should rollback the transaction if the callback fails", func(t *testing.T) { + t.Parallel() + + db, err := pgxmock.NewConn() + require.NoError(t, err) + t.Cleanup(func() { + db.Close(t.Context()) + }) + + transactor, _ := pgxTransactor.NewTransactor(db) + + db.ExpectBegin() + db.ExpectRollback() + + err = transactor.WithinTransaction(context.Background(), func(_ context.Context) error { + return errors.New("an error occurred") + }) + require.Error(t, err) + + require.NoError(t, db.ExpectationsWereMet()) + }) + + t.Run("it should commit the transaction if the callback succeeds", func(t *testing.T) { + t.Parallel() + + db, err := pgxmock.NewConn() + require.NoError(t, err) + t.Cleanup(func() { + db.Close(t.Context()) + }) + + transactor, _ := pgxTransactor.NewTransactor(db) + + db.ExpectBegin() + db.ExpectCommit() + + err = transactor.WithinTransaction(context.Background(), func(_ context.Context) error { + return nil + }) + require.NoError(t, err) + + require.NoError(t, db.ExpectationsWereMet()) + }) + + t.Run("it should return an error if the commit fails", func(t *testing.T) { + t.Parallel() + + db, err := pgxmock.NewConn() + require.NoError(t, err) + t.Cleanup(func() { + db.Close(t.Context()) + }) + + transactor, _ := pgxTransactor.NewTransactor(db) + + db.ExpectBegin() + db.ExpectCommit().WillReturnError(assert.AnError) + // Note: after a failed Commit, Rollback is called but doesn't reach the mock because + // the transaction is already marked as done. Rollback returns early with ErrTxDone. + + err = transactor.WithinTransaction(context.Background(), func(_ context.Context) error { + return nil + }) + require.Error(t, err) + + require.NoError(t, db.ExpectationsWereMet()) + }) +} + +func TestTransactor_IsWithinTransaction(t *testing.T) { t.Parallel() t.Run("it should return false if the context is not within a transaction", func(t *testing.T) { t.Parallel() + db, err := pgxmock.NewConn() + require.NoError(t, err) + t.Cleanup(func() { + db.Close(t.Context()) + }) + + transactor, _ := pgxTransactor.NewTransactor(db) + ctx := context.Background() + assert.False(t, transactor.IsWithinTransaction(ctx)) assert.False(t, pgxTransactor.IsWithinTransaction(ctx)) }) + + t.Run("it should return true if the context is within a transaction", func(t *testing.T) { + t.Parallel() + + db, err := pgxmock.NewConn() + require.NoError(t, err) + t.Cleanup(func() { + db.Close(t.Context()) + }) + + transactor, _ := pgxTransactor.NewTransactor(db) + + db.ExpectBegin() + db.ExpectCommit() + + err = transactor.WithinTransaction(context.Background(), func(ctx context.Context) error { + assert.True(t, transactor.IsWithinTransaction(ctx)) + assert.True(t, pgxTransactor.IsWithinTransaction(ctx)) + return nil + }) + require.NoError(t, err) + + require.NoError(t, db.ExpectationsWereMet()) + }) + + t.Run("it should return false if the context is within another transactor transaction", func(t *testing.T) { + t.Parallel() + + db, err := pgxmock.NewConn() + require.NoError(t, err) + t.Cleanup(func() { + db.Close(t.Context()) + }) + + transactorA, _ := pgxTransactor.NewTransactor(db) + transactorB, _ := pgxTransactor.NewTransactor(db) + + db.ExpectBegin() + db.ExpectCommit() + + err = transactorA.WithinTransaction(context.Background(), func(ctx context.Context) error { + assert.True(t, transactorA.IsWithinTransaction(ctx)) + assert.False(t, transactorB.IsWithinTransaction(ctx)) + assert.True(t, pgxTransactor.IsWithinTransaction(ctx)) + return nil + }) + require.NoError(t, err) + + require.NoError(t, db.ExpectationsWereMet()) + }) } diff --git a/tests/sqlx/transactor_test.go b/tests/sqlx/transactor_test.go index 4e5f570..101a3b4 100644 --- a/tests/sqlx/transactor_test.go +++ b/tests/sqlx/transactor_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" + "github.com/Thiht/transactor" sqlxTransactor "github.com/Thiht/transactor/sqlx" "github.com/jmoiron/sqlx" "github.com/stretchr/testify/assert" @@ -15,6 +16,11 @@ import ( func TestTransactor(t *testing.T) { t.Parallel() + t.Run("it should implement the Transactor interface", func(t *testing.T) { + t.Parallel() + assert.Implements(t, (*transactor.Transactor)(nil), &sqlxTransactor.Transactor{}) + }) + t.Run("it should rollback the transaction if the callback fails", func(t *testing.T) { t.Parallel() @@ -528,13 +534,23 @@ func TestTransactor(t *testing.T) { }) } -func TestIsWithinTransaction(t *testing.T) { +func TestTransactor_IsWithinTransaction(t *testing.T) { t.Parallel() t.Run("it should return false if the context is not within a transaction", func(t *testing.T) { t.Parallel() + db, _, err := sqlmock.New() + require.NoError(t, err) + t.Cleanup(func() { + db.Close() + }) + sqlxDB := sqlx.NewDb(db, "sqlmock") + + transactor, _ := sqlxTransactor.NewTransactor(sqlxDB, sqlxTransactor.NestedTransactionsNone) + ctx := context.Background() + assert.False(t, transactor.IsWithinTransaction(ctx)) assert.False(t, sqlxTransactor.IsWithinTransaction(ctx)) }) @@ -554,6 +570,34 @@ func TestIsWithinTransaction(t *testing.T) { mock.ExpectCommit() err = transactor.WithinTransaction(context.Background(), func(ctx context.Context) error { + assert.True(t, transactor.IsWithinTransaction(ctx)) + assert.True(t, sqlxTransactor.IsWithinTransaction(ctx)) + return nil + }) + require.NoError(t, err) + + require.NoError(t, mock.ExpectationsWereMet()) + }) + + t.Run("it should return false if the context is within another transactor transaction", func(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New() + require.NoError(t, err) + t.Cleanup(func() { + db.Close() + }) + sqlxDB := sqlx.NewDb(db, "sqlmock") + + transactorA, _ := sqlxTransactor.NewTransactor(sqlxDB, sqlxTransactor.NestedTransactionsNone) + transactorB, _ := sqlxTransactor.NewTransactor(sqlxDB, sqlxTransactor.NestedTransactionsNone) + + mock.ExpectBegin() + mock.ExpectCommit() + + err = transactorA.WithinTransaction(context.Background(), func(ctx context.Context) error { + assert.True(t, transactorA.IsWithinTransaction(ctx)) + assert.False(t, transactorB.IsWithinTransaction(ctx)) assert.True(t, sqlxTransactor.IsWithinTransaction(ctx)) return nil }) diff --git a/tests/stdlib/transactor_test.go b/tests/stdlib/transactor_test.go index ccfec05..b708f92 100644 --- a/tests/stdlib/transactor_test.go +++ b/tests/stdlib/transactor_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" + "github.com/Thiht/transactor" "github.com/Thiht/transactor/stdlib" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -14,6 +15,11 @@ import ( func TestTransactor(t *testing.T) { t.Parallel() + t.Run("it should implement the Transactor interface", func(t *testing.T) { + t.Parallel() + assert.Implements(t, (*transactor.Transactor)(nil), &stdlib.Transactor{}) + }) + t.Run("it should rollback the transaction if the callback fails", func(t *testing.T) { t.Parallel() @@ -510,13 +516,22 @@ func TestTransactor(t *testing.T) { }) } -func TestIsWithinTransaction(t *testing.T) { +func TestTransactor_IsWithinTransaction(t *testing.T) { t.Parallel() t.Run("it should return false if the context is not within a transaction", func(t *testing.T) { t.Parallel() + db, _, err := sqlmock.New() + require.NoError(t, err) + t.Cleanup(func() { + db.Close() + }) + + transactor, _ := stdlib.NewTransactor(db, stdlib.NestedTransactionsNone) + ctx := context.Background() + assert.False(t, transactor.IsWithinTransaction(ctx)) assert.False(t, stdlib.IsWithinTransaction(ctx)) }) @@ -535,6 +550,33 @@ func TestIsWithinTransaction(t *testing.T) { mock.ExpectCommit() err = transactor.WithinTransaction(context.Background(), func(ctx context.Context) error { + assert.True(t, transactor.IsWithinTransaction(ctx)) + assert.True(t, stdlib.IsWithinTransaction(ctx)) + return nil + }) + require.NoError(t, err) + + require.NoError(t, mock.ExpectationsWereMet()) + }) + + t.Run("it should return false if the context is within another transactor transaction", func(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New() + require.NoError(t, err) + t.Cleanup(func() { + db.Close() + }) + + transactorA, _ := stdlib.NewTransactor(db, stdlib.NestedTransactionsNone) + transactorB, _ := stdlib.NewTransactor(db, stdlib.NestedTransactionsNone) + + mock.ExpectBegin() + mock.ExpectCommit() + + err = transactorA.WithinTransaction(context.Background(), func(ctx context.Context) error { + assert.True(t, transactorA.IsWithinTransaction(ctx)) + assert.False(t, transactorB.IsWithinTransaction(ctx)) assert.True(t, stdlib.IsWithinTransaction(ctx)) return nil }) diff --git a/transactor.go b/transactor.go index 1464498..0025f69 100644 --- a/transactor.go +++ b/transactor.go @@ -7,4 +7,8 @@ type Transactor interface { // The transaction is added to the context, so it has to be retrieved // appropriately depending on the transactor implementation. WithinTransaction(context.Context, func(context.Context) error) error + + // IsWithinTransaction returns true if the context is within a transaction. + // Each transactor instance use a unique context key, so this method must be used with the correct transactor instance. + IsWithinTransaction(context.Context) bool }