Files
at-container-registry/pkg/hold/db/hold_db.go
T

102 lines
3.0 KiB
Go

package db
import (
"database/sql"
"fmt"
"io"
"log/slog"
"strings"
"time"
"github.com/tursodatabase/go-libsql"
)
// LibsqlConfig holds optional libSQL sync settings for embedded replicas.
// When SyncURL is empty, the database operates in local-only mode.
type LibsqlConfig struct {
SyncURL string
AuthToken string
SyncInterval time.Duration
}
// HoldDB wraps the shared *sql.DB and optional connector for lifecycle management.
type HoldDB struct {
DB *sql.DB
connector io.Closer // non-nil only in embedded replica mode
}
// Close closes the database connection and connector (if any).
// The connector must be closed to release file locks.
func (h *HoldDB) Close() error {
var dbErr, connErr error
if h.DB != nil {
dbErr = h.DB.Close()
}
if h.connector != nil {
connErr = h.connector.Close()
}
if dbErr != nil {
return dbErr
}
return connErr
}
// OpenHoldDB initializes the hold's shared database connection.
// Uses libSQL embedded replica when cfg.SyncURL is set, otherwise local-only.
// The caller must call HoldDB.Close() on shutdown.
func OpenHoldDB(path string, cfg LibsqlConfig) (*HoldDB, error) {
var db *sql.DB
var connector io.Closer
if cfg.SyncURL != "" {
// Embedded replica mode: local file + sync to remote
opts := []libsql.Option{
libsql.WithAuthToken(cfg.AuthToken),
}
if cfg.SyncInterval > 0 {
opts = append(opts, libsql.WithSyncInterval(cfg.SyncInterval))
}
conn, err := libsql.NewEmbeddedReplicaConnector(path, cfg.SyncURL, opts...)
if err != nil {
return nil, fmt.Errorf("failed to create libsql embedded replica connector: %w", err)
}
db = sql.OpenDB(conn)
connector = conn
slog.Info("Hold database opened in embedded replica mode", "path", path, "sync_url", cfg.SyncURL)
} else {
// Local-only mode: plain file via libsql driver
dsn := path
if !strings.HasPrefix(path, "file:") && !strings.HasPrefix(path, ":memory:") {
dsn = "file:" + path
}
var err error
db, err = sql.Open("libsql", dsn)
if err != nil {
return nil, fmt.Errorf("failed to open hold database: %w", err)
}
slog.Info("Hold database opened in local-only mode", "path", path)
}
// In local-only mode, configure WAL and busy_timeout locally.
// In embedded replica mode, the remote server manages these settings
// and PRAGMA assignments are rejected as "unsupported statement"
// (observed with Bunny Database).
if cfg.SyncURL == "" {
var journalMode string
if err := db.QueryRow("PRAGMA journal_mode = WAL").Scan(&journalMode); err != nil {
return nil, fmt.Errorf("failed to set journal mode: %w", err)
}
var busyTimeout int
if err := db.QueryRow("PRAGMA busy_timeout = 5000").Scan(&busyTimeout); err != nil {
return nil, fmt.Errorf("failed to set busy_timeout: %w", err)
}
}
// Foreign keys work in both modes
if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil {
return nil, fmt.Errorf("failed to enable foreign keys: %w", err)
}
return &HoldDB{DB: db, connector: connector}, nil
}