Last active
September 28, 2024 06:18
-
-
Save llimllib/2d03a8da46bbd3d247d2492849e3dc4e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package db | |
import ( | |
"context" | |
"database/sql" | |
"fmt" | |
"log/slog" | |
"net/url" | |
"os" | |
"runtime" | |
"time" | |
_ "github.com/mattn/go-sqlite3" | |
) | |
// reference: https://kerkour.com/sqlite-for-servers | |
type DB struct { | |
ReadDB *sql.DB | |
WriteDB *sql.DB | |
logger *slog.Logger | |
} | |
func NewDB(dbUrl string, logger *slog.Logger) (*DB, error) { | |
// we want to add a few parameters, so parse the db URL | |
readUrl, err := url.Parse(dbUrl) | |
if err != nil { | |
return nil, fmt.Errorf("error parsing connection string: %v", err) | |
} | |
// make a copy of the URL so we can safely append write params | |
writeUrl := *readUrl | |
// Add the _txlock=immediate flag | |
writeParams := writeUrl.Query() | |
writeParams.Add("_txlock", "immediate") | |
// Put sqlite in multithreaded mode; manage mutexes manually | |
// https://www.sqlite.org/threadsafe.html | |
writeParams.Add("_mutex", "no") | |
writeUrl.RawQuery = writeParams.Encode() | |
logger.Debug("connecting write db", "url", writeUrl.String()) | |
writeDB, err := sql.Open("sqlite3", writeUrl.String()) | |
if err != nil { | |
return nil, err | |
} | |
// We rely on this to provide our write locking. There should only be one | |
// open connection to the write database, and any further attempts will | |
// block until they acquire the lock | |
writeDB.SetMaxOpenConns(1) | |
setSQLitePragmas(writeDB) | |
// add readonly mode flag and open database | |
// docs on connection flags: | |
// https://pkg.go.dev/github.com/mattn/go-sqlite3#readme-connection-string | |
readParams := readUrl.Query() | |
readParams.Add("mode", "ro") | |
// Put sqlite in multithreaded mode; manage mutexes manually | |
// https://www.sqlite.org/threadsafe.html | |
readParams.Add("_mutex", "no") | |
readUrl.RawQuery = readParams.Encode() | |
logger.Debug("connecting read db", "url", readUrl.String()) | |
readDB, err := sql.Open("sqlite3", readUrl.String()) | |
if err != nil { | |
writeDB.Close() | |
return nil, err | |
} | |
readDB.SetMaxOpenConns(max(4, runtime.NumCPU())) | |
setSQLitePragmas(readDB) | |
return &DB{ | |
ReadDB: readDB, | |
WriteDB: writeDB, | |
logger: logger, | |
}, nil | |
} | |
// Make a query using the read connection | |
func (db *DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) { | |
t := time.Now() | |
rows, err := db.ReadDB.QueryContext(ctx, query, args...) | |
db.logger.Debug("querying", "query", query, "args", args, "duration", time.Since(t)) | |
return rows, err | |
} | |
// Make a query using the read connection and return the first row | |
func (db *DB) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row { | |
t := time.Now() | |
row := db.ReadDB.QueryRowContext(ctx, query, args...) | |
db.logger.Debug("querying row", "query", query, "args", args, "duration", time.Since(t)) | |
return row | |
} | |
// Execute a query using the write connection | |
func (db *DB) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { | |
// TODO: handle SQLITE_BUSY and retry in that case | |
t := time.Now() | |
res, err := db.WriteDB.ExecContext(ctx, query, args...) | |
if err != nil { | |
return nil, err | |
} | |
db.logger.Debug("executed", "query", query, "args", args, "duration", time.Since(t)) | |
return res, nil | |
} | |
// Close closes both read and write connections | |
func (db *DB) Close() error { | |
err1 := db.ReadDB.Close() | |
err2 := db.WriteDB.Close() | |
if err1 != nil { | |
return err1 | |
} | |
return err2 | |
} | |
// Helper functions | |
func must(_ any, err error) { | |
if err != nil { | |
panic(err) | |
} | |
} | |
func setSQLitePragmas(conn *sql.DB) { | |
pragmas := []string{ | |
"PRAGMA journal_mode = WAL;", | |
"PRAGMA busy_timeout = 5000;", | |
"PRAGMA synchronous = NORMAL;", | |
"PRAGMA cache_size = 1000000000;", | |
"PRAGMA foreign_keys = true;", | |
"PRAGMA temp_store = memory;", | |
} | |
for _, pragma := range pragmas { | |
must(conn.Exec(pragma)) | |
} | |
} | |
// RunSQLFile executes the SQL statements in the given file on the write | |
// connection | |
func (db *DB) RunSQLFile(filePath string) error { | |
sqlfile, err := os.ReadFile(filePath) | |
if err != nil { | |
return err | |
} | |
_, err = db.ExecContext(context.Background(), string(sqlfile)) | |
if err != nil { | |
return err | |
} | |
return nil | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment