carlos / platform Public

fix(console): apply durable SQLite settings on every connection

claude agent, operated by paul@keymail.dev eb2291399c36365bc963eb8e9a7329e7828f7e3f parent ed25653
2 files changed, +227 −16
  • internal/console/store.go +65 −16
  • internal/console/store_durability_test.go +162 −0

CI passed — run details

diff --git a/internal/console/store.go b/internal/console/store.go
index 62bacd848..564ad7591 100644
--- a/internal/console/store.go
+++ b/internal/console/store.go
@@ -18,6 +18,7 @@ import (
"errors"
"fmt"
"log"
+ "net/url"
"os"
"path/filepath"
"strings"
@@ -52,29 +53,23 @@ type Store struct {
// Open opens (creating if needed) the console DB at path.
func Open(path string) (*Store, error) {
- if dir := filepath.Dir(path); dir != "." && dir != "" {
- if err := os.MkdirAll(dir, 0o755); err != nil {
- return nil, err
+ if !strings.HasPrefix(path, "file:") {
+ if dir := filepath.Dir(path); dir != "." && dir != "" {
+ if err := os.MkdirAll(dir, 0o755); err != nil {
+ return nil, err
+ }
}
}
- db, err := sql.Open("sqlite", path)
+ dsn, err := consoleSQLiteDSN(path)
+ if err != nil {
+ return nil, err
+ }
+ db, err := sql.Open("sqlite", dsn)
if err != nil {
return nil, err
}
// One writer at a time keeps WAL happy.
db.SetMaxOpenConns(1)
- // busy_timeout FIRST: journal_mode=WAL takes a lock, and with no timeout
- // set yet a concurrent opener fails immediately with SQLITE_BUSY.
- for _, p := range []string{
- "PRAGMA busy_timeout=5000",
- "PRAGMA journal_mode=WAL",
- "PRAGMA synchronous=NORMAL",
- } {
- if _, err := db.Exec(p); err != nil {
- db.Close()
- return nil, err
- }
- }
s := &Store{db: db}
if err := s.migrate(); err != nil {
db.Close()
@@ -83,6 +78,60 @@ func Open(path string) (*Store, error) {
return s, nil
}
+// consoleSQLiteDSN puts the console's durability settings on every physical
+// connection opened by database/sql. A plain path is first made into a file
+// URI so query and fragment characters remain filename bytes rather than DSN
+// syntax. Explicit SQLite URIs retain their unrelated parameters.
+func consoleSQLiteDSN(path string) (string, error) {
+ if path == ":memory:" {
+ q := make(url.Values)
+ setConsoleSQLitePragmas(q)
+ return path + "?" + q.Encode(), nil
+ }
+ dsn := path
+ if !strings.HasPrefix(path, "file:") {
+ dsn = "file:" + (&url.URL{Path: path}).EscapedPath()
+ }
+ u, err := url.Parse(dsn)
+ if err != nil {
+ return "", err
+ }
+ q, err := url.ParseQuery(u.RawQuery)
+ if err != nil {
+ return "", err
+ }
+ setConsoleSQLitePragmas(q)
+ u.RawQuery = q.Encode()
+ return u.String(), nil
+}
+
+func setConsoleSQLitePragmas(q url.Values) {
+ for _, key := range []string{
+ "_busy_timeout", "_timeout",
+ "_journal_mode", "_journal",
+ "_synchronous", "_sync",
+ } {
+ q.Del(key)
+ }
+ pragmas := q["_pragma"][:0]
+ for _, pragma := range q["_pragma"] {
+ lower := strings.ToLower(pragma)
+ if !strings.Contains(lower, "busy_timeout") &&
+ !strings.Contains(lower, "journal_mode") &&
+ !strings.Contains(lower, "synchronous") {
+ pragmas = append(pragmas, pragma)
+ }
+ }
+ if len(pragmas) == 0 {
+ q.Del("_pragma")
+ } else {
+ q["_pragma"] = pragmas
+ }
+ q.Set("_busy_timeout", "5000")
+ q.Set("_journal_mode", "WAL")
+ q.Set("_synchronous", "FULL")
+}
+
// Close releases the underlying database handle.
func (s *Store) Close() error { return s.db.Close() }
diff --git a/internal/console/store_durability_test.go b/internal/console/store_durability_test.go
new file mode 100644
index 000000000..297ccd6d1
--- /dev/null
+++ b/internal/console/store_durability_test.go
@@ -0,0 +1,162 @@
+package console
+
+import (
+ "context"
+ "database/sql"
+ "database/sql/driver"
+ "errors"
+ "net/url"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+)
+
+func assertConsolePragmas(t *testing.T, s *Store, wantJournal string) {
+ t.Helper()
+ var busy, synchronous int
+ var journal string
+ if err := s.db.QueryRow(`PRAGMA busy_timeout`).Scan(&busy); err != nil {
+ t.Fatal(err)
+ }
+ if err := s.db.QueryRow(`PRAGMA synchronous`).Scan(&synchronous); err != nil {
+ t.Fatal(err)
+ }
+ if err := s.db.QueryRow(`PRAGMA journal_mode`).Scan(&journal); err != nil {
+ t.Fatal(err)
+ }
+ if busy != 5000 || synchronous != 2 || journal != wantJournal {
+ t.Fatalf("pragmas busy_timeout=%d synchronous=%d journal_mode=%q", busy, synchronous, journal)
+ }
+}
+
+func TestOpenAppliesDurabilityPragmasToReplacementConnections(t *testing.T) {
+ s, err := Open(filepath.Join(t.TempDir(), "console.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { s.Close() })
+ assertConsolePragmas(t, s, "wal")
+
+ // Returning ErrBadConn from Raw tells database/sql that this physical
+ // connection cannot be reused. The next query must open a replacement.
+ conn, err := s.db.Conn(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = conn.Raw(func(any) error { return driver.ErrBadConn })
+ if closeErr := conn.Close(); closeErr != nil && !errors.Is(closeErr, sql.ErrConnDone) {
+ t.Fatal(closeErr)
+ }
+ if !errors.Is(err, driver.ErrBadConn) {
+ t.Fatalf("discard connection: %v", err)
+ }
+ assertConsolePragmas(t, s, "wal")
+
+ now := time.Date(2026, 10, 4, 4, 40, 0, 0, time.UTC)
+ if err := s.CreateAccount(42, "cdef", "Account", overageOwner, now); err != nil {
+ t.Fatal(err)
+ }
+ receipt, err := s.changeOveragePreference(overageChange(42, overageOwner, 0, false, 61), now.Add(time.Second))
+ if err != nil || receipt.Revision != 1 || receipt.RequestedOn {
+ t.Fatalf("replacement-connection OFF receipt=%+v err=%v", receipt, err)
+ }
+}
+
+func TestOpenOverridesConflictingExplicitURIPragmas(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ query string
+ }{
+ {
+ name: "shorthand aliases",
+ query: "_timeout=1&_journal=OFF&_sync=OFF",
+ },
+ {
+ name: "pragma list",
+ query: "_pragma=busy_timeout%281%29&_pragma=journal_mode%28OFF%29&_pragma=synchronous%28NORMAL%29",
+ },
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "console.db")
+ dsn := "file:" + (&url.URL{Path: path}).EscapedPath() + "?mode=rwc&" + tc.query
+ s, err := Open(dsn)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { s.Close() })
+ assertConsolePragmas(t, s, "wal")
+ })
+ }
+}
+
+func TestOpenEscapesFilenameDSNSyntax(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "console ?#& name.db")
+ s, err := Open(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := s.Close(); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := os.Stat(path); err != nil {
+ t.Fatalf("database was not created at the literal path %q: %v", path, err)
+ }
+}
+
+func TestOpenPreservesMemoryDatabase(t *testing.T) {
+ s, err := Open(":memory:")
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { s.Close() })
+ assertConsolePragmas(t, s, "memory")
+ var tables int
+ if err := s.db.QueryRow(`SELECT count(*) FROM sqlite_master WHERE type='table' AND name='accounts'`).Scan(&tables); err != nil {
+ t.Fatal(err)
+ }
+ if tables != 1 {
+ t.Fatalf("migrations in memory: accounts tables=%d", tables)
+ }
+}
+
+func TestOpenPreservesExplicitFileURI(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "uri ?#& name.db")
+ dsn := "file:" + (&url.URL{Path: path}).EscapedPath() + "?mode=rwc&cache=private&_foreign_keys=on&_pragma=cache_size%28123%29"
+ s, err := Open(dsn)
+ if err != nil {
+ t.Fatal(err)
+ }
+ assertConsolePragmas(t, s, "wal")
+ var foreignKeys, cacheSize int
+ if err := s.db.QueryRow(`PRAGMA foreign_keys`).Scan(&foreignKeys); err != nil {
+ t.Fatal(err)
+ }
+ if err := s.db.QueryRow(`PRAGMA cache_size`).Scan(&cacheSize); err != nil {
+ t.Fatal(err)
+ }
+ if foreignKeys != 1 || cacheSize != 123 {
+ t.Fatalf("unrelated URI pragmas foreign_keys=%d cache_size=%d", foreignKeys, cacheSize)
+ }
+ if err := s.Close(); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := os.Stat(path); err != nil {
+ t.Fatalf("database URI did not retain its literal path %q: %v", path, err)
+ }
+}
+
+func TestOpenRejectsMalformedExplicitURIQueryBeforeCreatingDatabase(t *testing.T) {
+ for _, query := range []string{"mode=ro%zz", "mode=rwc;cache=shared"} {
+ t.Run(query, func(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "must-not-exist.db")
+ dsn := "file:" + (&url.URL{Path: path}).EscapedPath() + "?" + query
+ if _, err := Open(dsn); err == nil {
+ t.Fatal("Open accepted malformed URI query")
+ }
+ if _, err := os.Stat(path); !os.IsNotExist(err) {
+ t.Fatalf("malformed URI created database %q: %v", path, err)
+ }
+ })
+ }
+}