hako/internal/database/connections.go
2026-01-03 18:05:41 +01:00

184 lines
5.1 KiB
Go

package database
import (
"context"
"database/sql"
"fmt"
"strings"
"git.nakama.town/fmartingr/gotoolkit/model"
)
// Connections holds read and write database connections and implements model.DB
type Connections struct {
Read *sql.DB
Write *sql.DB
}
// Ensure Connections implements model.DB interface
var _ model.DB = (*Connections)(nil)
// NewConnections creates read and write database connections
func NewConnections(readURL, writeURL string) (*Connections, error) {
readDB, err := connectDatabase(readURL, true)
if err != nil {
return nil, fmt.Errorf("failed to connect to read database: %w", err)
}
writeDB, err := connectDatabase(writeURL, false)
if err != nil {
_ = readDB.Close()
return nil, fmt.Errorf("failed to connect to write database: %w", err)
}
// Configure SQLite for better performance
if err := configureSQLite(readDB, true); err != nil {
_ = readDB.Close()
_ = writeDB.Close()
return nil, fmt.Errorf("failed to configure read database: %w", err)
}
if err := configureSQLite(writeDB, false); err != nil {
_ = readDB.Close()
_ = writeDB.Close()
return nil, fmt.Errorf("failed to configure write database: %w", err)
}
return &Connections{
Read: readDB,
Write: writeDB,
}, nil
}
// Close closes all database connections
func (c *Connections) Close() error {
var errs []error
if c.Read != nil {
if err := c.Read.Close(); err != nil {
errs = append(errs, fmt.Errorf("failed to close read connection: %w", err))
}
}
if c.Write != nil {
if err := c.Write.Close(); err != nil {
errs = append(errs, fmt.Errorf("failed to close write connection: %w", err))
}
}
if len(errs) > 0 {
return fmt.Errorf("errors closing connections: %v", errs)
}
return nil
}
// connectDatabase connects to the database using the connection string
func connectDatabase(dbURL string, readOnly bool) (*sql.DB, error) {
// Parse the database URL
// Format: scheme:path (e.g., sqlite:./hako.db)
// For SQLite, we need to extract the path after the colon
var dsn string
if len(dbURL) > 7 && dbURL[:7] == "sqlite:" {
dsn = dbURL[7:]
} else {
return nil, fmt.Errorf("unsupported database scheme in URL: %s", dbURL)
}
// Add read-only mode for read connections
if readOnly {
// Check if DSN already has query parameters
if strings.Contains(dsn, "?") {
dsn += "&mode=ro"
} else {
dsn += "?mode=ro"
}
}
db, err := sql.Open("sqlite", dsn)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
return db, nil
}
// configureSQLite configures SQLite connection for optimal performance
func configureSQLite(db *sql.DB, readOnly bool) error {
if readOnly {
// For read-only connections, we can use more aggressive settings
_, err := db.Exec("PRAGMA journal_mode=WAL")
if err != nil {
return fmt.Errorf("failed to set WAL mode: %w", err)
}
_, err = db.Exec("PRAGMA synchronous=NORMAL")
if err != nil {
return fmt.Errorf("failed to set synchronous mode: %w", err)
}
_, err = db.Exec("PRAGMA cache_size=-64000") // 64MB cache
if err != nil {
return fmt.Errorf("failed to set cache size: %w", err)
}
} else {
// For write connections, use WAL mode for better concurrency
_, err := db.Exec("PRAGMA journal_mode=WAL")
if err != nil {
return fmt.Errorf("failed to set WAL mode: %w", err)
}
_, err = db.Exec("PRAGMA synchronous=NORMAL")
if err != nil {
return fmt.Errorf("failed to set synchronous mode: %w", err)
}
_, err = db.Exec("PRAGMA cache_size=-64000") // 64MB cache
if err != nil {
return fmt.Errorf("failed to set cache size: %w", err)
}
_, err = db.Exec("PRAGMA foreign_keys=ON")
if err != nil {
return fmt.Errorf("failed to enable foreign keys: %w", err)
}
// Set connection pool settings for write connection
db.SetMaxOpenConns(1) // SQLite works best with single writer
db.SetMaxIdleConns(1)
}
// Set connection pool settings for read connection
if readOnly {
db.SetMaxOpenConns(10) // Multiple readers allowed
db.SetMaxIdleConns(5)
}
return nil
}
// WriterDB returns the write database connection
func (c *Connections) WriterDB() *sql.DB {
return c.Write
}
// ReaderDB returns the read database connection
func (c *Connections) ReaderDB() *sql.DB {
return c.Read
}
// GetDatabaseSchemaVersion returns the current database schema version
func (c *Connections) GetDatabaseSchemaVersion(ctx context.Context) (string, error) {
var version string
err := c.Read.QueryRowContext(ctx, "SELECT version FROM schema_version ORDER BY id DESC LIMIT 1").Scan(&version)
if err != nil {
if err == sql.ErrNoRows {
return "", nil // No version means fresh database
}
return "", fmt.Errorf("failed to get schema version: %w", err)
}
return version, nil
}
// SetDatabaseSchemaVersion sets the database schema version
func (c *Connections) SetDatabaseSchemaVersion(ctx context.Context, version string) error {
_, err := c.Write.ExecContext(ctx, "INSERT INTO schema_version (version) VALUES (?)", version)
if err != nil {
return fmt.Errorf("failed to set schema version: %w", err)
}
return nil
}