Files
orm/db/pragma_test.go
zeekayandHanzo Dev 692d49366b fix(sqlite): route orm opens through sqlite.PragmaDSN — pragmas were dropped under modernc
buildPragmas() emitted mattn-only DSN syntax (`_busy_timeout=`, `_journal_mode=`)
on a bare `path?...` DSN. Under modernc — the CGO=0 backend every pure-Go auth
service (IAM/ATS/BD/TA/AML/KMS + base/kms/tasks/cloud) runs — those params are
SILENTLY IGNORED, so the DB opened with busy_timeout=0 + journal_mode=DELETE +
foreign_keys=0. Concurrent writers then hit immediate SQLITE_BUSY ("database is
locked") → intermittent 500s under auth load. The v0.5.3 convergence fixed the
driver registration but left this DSN bug.

Route both read/write opens through sqlite.PragmaDSN(path, configPragmas(cfg)),
which encodes the pragma set in the ACTIVE backend's syntax (modernc
`_pragma=name(value)` / mattn `_name=value`). configPragmas maps SQLiteConfig
(honoring overrides) with safe defaults so busy_timeout=10000 + WAL ALWAYS apply.

Proven under modernc (CGO=0):
  OLD bare mattn DSN : journal_mode=delete busy_timeout=0     foreign_keys=0
  NEW PragmaDSN      : journal_mode=wal    busy_timeout=10000 foreign_keys=1
db/pragma_test.go is the permanent regression guard (green both backends).

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-03 22:37:35 -07:00

72 lines
2.2 KiB
Go

package db
import (
"database/sql"
"path/filepath"
"strings"
"testing"
"github.com/hanzoai/sqlite"
)
// TestConfigPragmasApply proves the SQLite open path applies busy_timeout + WAL +
// foreign_keys on the ACTIVE backend — the regression guard for the bug where
// orm emitted mattn-only `_busy_timeout=` DSN params that modernc silently
// dropped (busy_timeout=0, journal_mode=DELETE → SQLITE_BUSY under concurrent
// writers). Runs green under BOTH build tags; in CGO=0 CI it exercises modernc,
// the backend the CGO=0 auth services run.
func TestConfigPragmasApply(t *testing.T) {
dsn := sqlite.PragmaDSN(filepath.Join(t.TempDir(), "orm.db"), configPragmas(SQLiteConfig{}))
db, err := sql.Open("sqlite", dsn)
if err != nil {
t.Fatalf("open: %v", err)
}
defer db.Close()
if err := db.Ping(); err != nil {
t.Fatalf("ping: %v", err)
}
var journal string
if err := db.QueryRow(`PRAGMA journal_mode`).Scan(&journal); err != nil {
t.Fatalf("journal_mode: %v", err)
}
if !strings.EqualFold(journal, "wal") {
t.Fatalf("journal_mode = %q, want wal (pragmas dropped on this backend)", journal)
}
var busy int
if err := db.QueryRow(`PRAGMA busy_timeout`).Scan(&busy); err != nil {
t.Fatalf("busy_timeout: %v", err)
}
if busy != 10000 {
t.Fatalf("busy_timeout = %d, want 10000 (concurrent writers would get SQLITE_BUSY)", busy)
}
var fk int
if err := db.QueryRow(`PRAGMA foreign_keys`).Scan(&fk); err != nil {
t.Fatalf("foreign_keys: %v", err)
}
if fk != 1 {
t.Fatalf("foreign_keys = %d, want 1", fk)
}
}
// TestConfigPragmasHonorOverrides proves explicit SQLiteConfig values win over
// the defaults (and still apply on the active backend).
func TestConfigPragmasHonorOverrides(t *testing.T) {
cfg := SQLiteConfig{BusyTimeout: 4200, JournalMode: "WAL"}
dsn := sqlite.PragmaDSN(filepath.Join(t.TempDir(), "orm2.db"), configPragmas(cfg))
db, err := sql.Open("sqlite", dsn)
if err != nil {
t.Fatalf("open: %v", err)
}
defer db.Close()
var busy int
if err := db.QueryRow(`PRAGMA busy_timeout`).Scan(&busy); err != nil {
t.Fatalf("busy_timeout: %v", err)
}
if busy != 4200 {
t.Fatalf("busy_timeout = %d, want 4200 (config override ignored)", busy)
}
}