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 // }