Files
go-admin/common/database/sqlite_dsn.go
T
zhangwenjian 8a1218c404 fix🐛: only treat exact in-memory forms as in-memory sqlite sources
A file whose name merely contained :memory: or mode=memory was classified as an
in-memory database and skipped the locking defaults. Parse the query and match
the exact filename forms or a mode=memory parameter.
2026-10-07 17:40:49 +08:00

78 lines
2.5 KiB
Go

package database
import (
"net/url"
"strings"
"gorm.io/gorm"
)
// sqlitePragmas are the connection parameters a file-backed sqlite database
// needs to behave under a server that issues concurrent requests. Each one is
// applied only when the configured source does not already set it.
//
// Without them sqlite runs in rollback-journal mode with no busy timeout: a
// writer blocks every reader and a second writer fails at once with
// "database is locked", instead of waiting for the first to finish.
//
// _busy_timeout wait up to 10s for a lock rather than failing immediately
// _journal_mode WAL lets readers proceed while a write is in progress
// _synchronous NORMAL is the pairing WAL is designed for
// _txlock a transaction takes the write lock when it begins. The
// default defers it, and a deferred transaction that reads
// and then writes is refused with SQLITE_BUSY straight away -
// busy_timeout does not apply to that upgrade.
var sqlitePragmas = [][2]string{
{"_busy_timeout", "10000"},
{"_journal_mode", "WAL"},
{"_synchronous", "NORMAL"},
{"_txlock", "immediate"},
}
// sqliteDSN returns source with the defaults in sqlitePragmas added.
// In-memory databases are returned untouched: WAL has no meaning there.
func sqliteDSN(source string) string {
if source == "" {
return source
}
path, rawQuery := source, ""
if i := strings.IndexByte(source, '?'); i >= 0 {
path, rawQuery = source[:i], source[i+1:]
}
q, err := url.ParseQuery(rawQuery)
if err != nil {
// Not something to rewrite blind; the driver will report it.
return source
}
if isMemoryDB(path, q) {
return source
}
for _, p := range sqlitePragmas {
if _, set := q[p[0]]; !set {
q.Set(p[0], p[1])
}
}
return path + "?" + q.Encode()
}
// isMemoryDB reports whether the source names an in-memory database: the exact
// filename forms sqlite recognises, or a mode=memory parameter. A file that
// merely has ":memory:" or "mode=memory" somewhere in its name is a file.
func isMemoryDB(path string, q url.Values) bool {
switch path {
case ":memory:", "file::memory:", "file:":
return true
}
return q.Get("mode") == "memory"
}
// withSqliteDefaults wraps open so every connection string handed to a sqlite3
// dialector - the primary and any replicas - carries the defaults above.
func withSqliteDefaults(driver string, open func(string) gorm.Dialector) func(string) gorm.Dialector {
if driver != "sqlite3" {
return open
}
return func(dsn string) gorm.Dialector { return open(sqliteDSN(dsn)) }
}