git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/sqlitex/db.go (about)

     1  package sqlitex
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"fmt"
     7  	"runtime"
     8  
     9  	"github.com/jmoiron/sqlx"
    10  )
    11  
    12  type DB struct {
    13  	writeDB *sqlx.DB
    14  	readDB  *sqlx.DB
    15  }
    16  
    17  // ensures at compile time that DB satisfies the Queryer interface
    18  var _ Queryer = (*DB)(nil)
    19  
    20  type DBOptions struct {
    21  	BusyTimeout int64
    22  }
    23  
    24  type Queryer interface {
    25  	Get(ctx context.Context, dest any, query string, args ...any) error
    26  	Select(ctx context.Context, dest any, query string, args ...any) error
    27  	Query(ctx context.Context, query string, args ...any) (*sql.Rows, error)
    28  	Exec(ctx context.Context, query string, args ...any) (sql.Result, error)
    29  	// Rebind(query string) (ret string)
    30  }
    31  
    32  func Open(file string, options *DBOptions) (db *DB, err error) {
    33  	connectionUrl := fmt.Sprintf("%s?_journal_mode=WAL&_busy_timeout=10000&_synchronous=NORMAL&_txlock=immediate&_cache_size=50000000&_foreign_keys=true", file)
    34  
    35  	writeDB, err := sqlx.Open("sqlite3", connectionUrl)
    36  	if err != nil {
    37  		err = fmt.Errorf("sqlitex: error openning write connection: %w", err)
    38  		return
    39  	}
    40  	writeDB.SetMaxOpenConns(1)
    41  	err = writeDB.Ping()
    42  	if err != nil {
    43  		err = fmt.Errorf("sqlitex: error establishing write connection: %w", err)
    44  		return
    45  	}
    46  
    47  	readDB, err := sqlx.Open("sqlite3", connectionUrl)
    48  	if err != nil {
    49  		err = fmt.Errorf("sqlitex: error openning read connection: %w", err)
    50  		return
    51  	}
    52  	readDB.SetMaxOpenConns(4)
    53  	err = readDB.Ping()
    54  	if err != nil {
    55  		err = fmt.Errorf("sqlitex: error establishing read connection: %w", err)
    56  		return
    57  	}
    58  
    59  	db = &DB{
    60  		writeDB: writeDB,
    61  		readDB:  readDB,
    62  	}
    63  
    64  	return
    65  }
    66  
    67  func (db *DB) Close() (err error) {
    68  	err = db.writeDB.Close()
    69  	if err != nil {
    70  		return
    71  	}
    72  
    73  	err = db.readDB.Close()
    74  	if err != nil {
    75  		return
    76  	}
    77  
    78  	return
    79  }
    80  
    81  // Ping verifies a connection to the database is still alive, establishing a connection if necessary.
    82  // func (db *DB) Ping(ctx context.Context) error {
    83  // 	return db.readDB.PingContext(ctx)
    84  // }
    85  
    86  // Stats returns database statistics.
    87  // func (db *DB) Stats() sql.DBStats {
    88  // 	return db.readDB.Stats()
    89  // }
    90  
    91  // Begin starts a transaction. The default isolation level is dependent on the driver.
    92  // The provided context is used until the transaction is committed or rolled back. If the context is
    93  // canceled, the sql package will roll back the transaction. Tx.Commit will return an error if the
    94  // context provided to BeginTx is canceled.
    95  // func (db *DB) Begin(ctx context.Context) (Tx, error) {
    96  // 	sqlxTx, err := db.sqlxDB.BeginTxx(ctx, nil)
    97  // 	return &Transaction{sqlxTx}, err
    98  // }
    99  
   100  // func (db *DB) Rebind(query string) (ret string) {
   101  // 	return db.sqlxDB.Rebind(query)
   102  // }
   103  
   104  // BeginTx starts a transaction.
   105  //
   106  // The provided context is used until the transaction is committed or rolled back. If the context is
   107  // canceled, the sql package will roll back the transaction. Tx.Commit will return an error if the
   108  // context provided to BeginTx is canceled.
   109  //
   110  // The provided TxOptions is optional and may be nil if defaults should be used. If a non-default
   111  // isolation level is used that the driver doesn't support, an error will be returned.
   112  // func (db *DB) BeginTx(ctx context.Context, opts *sql.TxOptions) (Tx, error) {
   113  // 	sqlxTx, err := db.sqlxDB.BeginTxx(ctx, opts)
   114  // 	return &Transaction{sqlxTx}, err
   115  // }
   116  
   117  // Exec executes a query without returning any rows. The args are for any placeholder parameters in the query.
   118  func (db *DB) Exec(ctx context.Context, query string, args ...any) (sql.Result, error) {
   119  	return db.writeDB.ExecContext(ctx, query, args...)
   120  }
   121  
   122  // Get a single record. Any placeholder parameters are replaced with supplied args. An `ErrNoRows`
   123  // error is returned if the result set is empty.
   124  func (db *DB) Get(ctx context.Context, dest any, query string, args ...any) error {
   125  	return db.readDB.GetContext(ctx, dest, query, args...)
   126  }
   127  
   128  // Query executes a query that returns rows, typically a SELECT. The args are for any placeholder
   129  // parameters in the query.
   130  func (db *DB) Query(ctx context.Context, query string, args ...any) (*sql.Rows, error) {
   131  	return db.readDB.QueryContext(ctx, query, args...)
   132  }
   133  
   134  // Select an array of records. Any placeholder parameters are replaced with supplied args.
   135  func (db *DB) Select(ctx context.Context, dest any, query string, args ...any) error {
   136  	return db.readDB.SelectContext(ctx, dest, query, args...)
   137  }
   138  
   139  func (db *DB) Transaction(ctx context.Context, fn func(tx *Tx) error) (err error) {
   140  	sqlxTx, err := db.writeDB.BeginTxx(ctx, nil)
   141  	if err != nil {
   142  		return err
   143  	}
   144  
   145  	tx := &Tx{sqlxTx}
   146  
   147  	defer func() {
   148  		if panicErr := recover(); panicErr != nil {
   149  			_, filename, line, _ := runtime.Caller(2)
   150  			rollbackErr := tx.Rollback()
   151  			if rollbackErr != nil {
   152  				err = fmt.Errorf("db: panic (%s:%d) in transaction + rollback: %v, %w", filename, line, panicErr, rollbackErr)
   153  			} else {
   154  				err = fmt.Errorf("db: panic (%s:%d) in transaction: %v", filename, line, panicErr)
   155  			}
   156  		}
   157  
   158  		if err == nil {
   159  			err = tx.Commit()
   160  		} else {
   161  			if rollbackErr := tx.Rollback(); rollbackErr != nil {
   162  				err = fmt.Errorf("db: transaction error: %w, rollback err: %w", err, rollbackErr)
   163  			}
   164  		}
   165  	}()
   166  
   167  	err = fn(tx)
   168  
   169  	return err
   170  }
   171  
   172  // Transaction is wrapper of `sqlx.Tx` which implements `Tx`
   173  type Tx struct {
   174  	sqlxTx *sqlx.Tx
   175  }
   176  
   177  // ensure at compile time that Tx satisfies the Queryer interface
   178  var _ Queryer = (*Tx)(nil)
   179  
   180  // Commit commits the transaction.
   181  func (tx *Tx) Commit() error {
   182  	return tx.sqlxTx.Commit()
   183  }
   184  
   185  // Rollback aborts the transaction.
   186  func (tx *Tx) Rollback() error {
   187  	return tx.sqlxTx.Rollback()
   188  }
   189  
   190  // Exec executes a query without returning any rows. The args are for any placeholder parameters in the query.
   191  func (tx *Tx) Exec(ctx context.Context, query string, args ...any) (sql.Result, error) {
   192  	return tx.sqlxTx.ExecContext(ctx, query, args...)
   193  }
   194  
   195  // Get a single record. Any placeholder parameters are replaced with supplied args. An `ErrNoRows`
   196  // error is returned if the result set is empty.
   197  func (tx *Tx) Get(ctx context.Context, dest any, query string, args ...any) error {
   198  	return tx.sqlxTx.GetContext(ctx, dest, query, args...)
   199  }
   200  
   201  // Query executes a query that returns rows, typically a SELECT. The args are for any placeholder
   202  // parameters in the query.
   203  func (tx *Tx) Query(ctx context.Context, query string, args ...any) (*sql.Rows, error) {
   204  	return tx.sqlxTx.QueryContext(ctx, query, args...)
   205  }
   206  
   207  // Select an array of records. Any placeholder parameters are replaced with supplied args.
   208  func (tx *Tx) Select(ctx context.Context, dest any, query string, args ...any) error {
   209  	return tx.sqlxTx.SelectContext(ctx, dest, query, args...)
   210  }
   211  
   212  // func (tx *Tx) Rebind(query string) (ret string) {
   213  // 	return tx.sqlxTx.Rebind(query)
   214  // }