diff --git a/coderd/database/db.go b/coderd/database/db.go index 5236b18d65c82..0a9e8928df253 100644 --- a/coderd/database/db.go +++ b/coderd/database/db.go @@ -47,9 +47,17 @@ type sqlQuerier struct { // InTx performs database operations inside a transaction. func (q *sqlQuerier) InTx(function func(Store) error) error { - if q.sdb == nil { + if _, ok := q.db.(*sql.Tx); ok { + // If the current inner "db" is already a transaction, we just reuse it. + // We do not need to handle commit/rollback as the outer tx will handle + // that. + err := function(q) + if err != nil { + return xerrors.Errorf("execute transaction: %w", err) + } return nil } + transaction, err := q.sdb.Begin() if err != nil { return xerrors.Errorf("begin transaction: %w", err) diff --git a/coderd/database/db_test.go b/coderd/database/db_test.go new file mode 100644 index 0000000000000..bb8288d8ed38e --- /dev/null +++ b/coderd/database/db_test.go @@ -0,0 +1,46 @@ +//go:build linux + +package database_test + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/coderd/database" +) + +func TestNestedInTx(t *testing.T) { + t.Parallel() + + uid := uuid.New() + sqlDB := testSQLDB(t) + err := database.MigrateUp(sqlDB) + require.NoError(t, err, "migrations") + + db := database.New(sqlDB) + err = db.InTx(func(outer database.Store) error { + return outer.InTx(func(inner database.Store) error { + //nolint:gocritic + require.Equal(t, outer, inner, "should be same transaction") + + _, err := inner.InsertUser(context.Background(), database.InsertUserParams{ + ID: uid, + Email: "coder@coder.com", + Username: "coder", + HashedPassword: []byte{}, + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + RBACRoles: []string{}, + }) + return err + }) + }) + require.NoError(t, err, "outer tx: %w", err) + + user, err := db.GetUserByID(context.Background(), uid) + require.NoError(t, err, "user exists") + require.Equal(t, uid, user.ID, "user id expected") +}