package dbi import ( "context" "database/sql" "errors" "log/slog" ) type Factory[T any] func(DBI) T type DBI interface { Query(context.Context, string, ...any) (*sql.Rows, error) QueryRow(context.Context, string, ...any) *sql.Row Exec(context.Context, string, ...any) (sql.Result, error) } type TxDal struct { Tx *sql.Tx } var _ DBI = &TxDal{} func (t *TxDal) Query(ctx context.Context, query string, args ...any) (*sql.Rows, error) { return t.Tx.QueryContext(ctx, query, args...) } func (t *TxDal) QueryRow(ctx context.Context, query string, args ...any) *sql.Row { return t.Tx.QueryRowContext(ctx, query, args...) } func (t *TxDal) Exec(ctx context.Context, query string, args ...any) (sql.Result, error) { return t.Tx.ExecContext(ctx, query, args...) } // ReadWrite executes fn within a read-write transaction. // The transaction is committed if fn returns nil, otherwise rolled back. func ReadWrite[T any](ctx context.Context, db *sql.DB, fn func(DBI) (T, error)) (T, error) { return WithTx(ctx, db, &sql.TxOptions{}, func(tx *TxDal) (T, error) { return fn(tx) }) } // ReadOnly executes fn within a read-only transaction. // The transaction is committed if fn returns nil, otherwise rolled back. func ReadOnly[T any](ctx context.Context, db *sql.DB, fn func(DBI) (T, error)) (T, error) { return WithTx(ctx, db, &sql.TxOptions{ReadOnly: true}, func(tx *TxDal) (T, error) { return fn(tx) }) } func WithTx[T any](ctx context.Context, db *sql.DB, opts *sql.TxOptions, fn func(*TxDal) (T, error)) (T, error) { var zero T // TODO does this call begin transaction immediate on sqlite? tx, err := db.BeginTx(ctx, opts) if err != nil { return zero, err } defer func() { err := tx.Rollback() if err != nil && !errors.Is(err, sql.ErrTxDone) { slog.Error("rolling back tx", "err", err) } }() t, err := fn(&TxDal{tx}) if err != nil { return zero, err } return t, tx.Commit() } func TranslateNotFound(from, to error) error { if errors.Is(from, sql.ErrNoRows) { return to } return from }