1package db23import (4 "context"5 "database/sql"6 "errors"7 "fmt"89 "github.com/charmbracelet/log/v2"10 "github.com/charmbracelet/soft-serve/pkg/config"11 "github.com/jmoiron/sqlx"12 _ "modernc.org/sqlite" // sqlite driver13)1415// DB is the interface for a Soft Serve database.16type DB struct {17 *sqlx.DB18 logger *log.Logger19}2021// Open opens a database connection.22func Open(ctx context.Context, driverName string, dsn string) (*DB, error) {23 db, err := sqlx.ConnectContext(ctx, driverName, dsn)24 if err != nil {25 return nil, err26 }2728 d := &DB{29 DB: db,30 }3132 if config.IsVerbose() {33 logger := log.FromContext(ctx).WithPrefix("db")34 d.logger = logger35 }3637 return d, nil38}3940// Close implements db.DB.41func (d *DB) Close() error {42 return d.DB.Close()43}4445// Tx is a database transaction.46type Tx struct {47 *sqlx.Tx48 logger *log.Logger49}5051// Transaction implements db.DB.52func (d *DB) Transaction(fn func(tx *Tx) error) error {53 return d.TransactionContext(context.Background(), fn)54}5556// TransactionContext implements db.DB.57func (d *DB) TransactionContext(ctx context.Context, fn func(tx *Tx) error) error {58 txx, err := d.DB.BeginTxx(ctx, nil)59 if err != nil {60 return fmt.Errorf("failed to begin transaction: %w", err)61 }6263 tx := &Tx{txx, d.logger}64 if err := fn(tx); err != nil {65 return rollback(tx, err)66 }6768 if err := tx.Commit(); err != nil {69 if errors.Is(err, sql.ErrTxDone) {70 // this is ok because whoever did finish the tx should have also written the error already.71 return nil72 }73 return fmt.Errorf("failed to commit transaction: %w", err)74 }7576 return nil77}7879func rollback(tx *Tx, err error) error {80 if rerr := tx.Rollback(); rerr != nil {81 if errors.Is(rerr, sql.ErrTxDone) {82 return err83 }84 return fmt.Errorf("failed to rollback: %s: %w", err.Error(), rerr)85 }8687 return err88}