mirror of
https://github.com/hanzoai/sqlite.git
synced 2026-10-30 18:22:26 +00:00
Two backend-neutral primitives so hanzoai/replicate can route through hanzoai/sqlite and stop blank-importing modernc.org/sqlite — which under CGO=1 registers 'sqlite' a SECOND time (mattn already registered it), the live 'sql: Register called twice for driver sqlite' panic in base/store/replicator (base -> commerce -> cloud all embed replicate, so this would panic the cgo cloud binary at startup). - SetPersistWAL(rawConn, on): SQLITE_FCNTL_PERSIST_WAL via mattn SetFileControlInt (cgo) / modernc FileControl.FileControlPersistWAL (nocgo). Keeps -wal across close (WAL-shipping needs it). Proven: -wal survives db.Close() on both backends. - OpenPragma(dsn, pragmas): a database/sql Connector applying pragmas to EVERY pooled connection at connect. wal_autocheckpoint is the reason: mattn drops it from the DSN, so a naive port would re-enable auto-checkpoint and truncate the WAL under a replicator (data loss). Per-connection ExecContext is backend-neutral. Proven on 3 concurrent conns under both backends (busy_timeout/journal_mode/wal_autocheckpoint/ foreign_keys all applied per-conn). Tag-neutral driver resolution. All three tag combos green (modernc, mattn, mattn+sqlite_purego); vet clean. LLM.md -> v0.2.3.
92 lines
3.2 KiB
Go
92 lines
3.2 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"database/sql/driver"
|
|
"fmt"
|
|
"sync"
|
|
)
|
|
|
|
// OpenPragma opens a *sql.DB on the registered "sqlite" backend where EVERY
|
|
// pooled connection has the given pragmas applied, in order, via PRAGMA
|
|
// statements run at connect time.
|
|
//
|
|
// Use it for pragmas that must hold on every connection but that the two
|
|
// backends do NOT accept uniformly in the DSN. The load-bearing case is
|
|
// wal_autocheckpoint: modernc honors `?_pragma=wal_autocheckpoint(0)` in the
|
|
// DSN, but mattn silently DROPS it (mattn's DSN grammar has no such param), so a
|
|
// binary built CGO=1 would re-enable auto-checkpoint and truncate the WAL out
|
|
// from under a WAL-shipping replicator — losing committed frames. Running the
|
|
// pragma per connection via ExecContext is backend-neutral (both mattn and
|
|
// modernc conns implement driver.ExecerContext), so it takes effect either way.
|
|
//
|
|
// Order matters: put busy_timeout first so a connection blocks on a busy
|
|
// database before journal_mode=WAL is attempted (WAL can't be set while another
|
|
// connection holds the db).
|
|
//
|
|
// dsn is a plain DSN — typically "file:"+path, or DSN(path, key) when the file
|
|
// is SQLCipher-encrypted (the key must ride the DSN; pragmas can't carry it).
|
|
// Do NOT also put these pragmas in the DSN; they're applied here.
|
|
func OpenPragma(dsn string, pragmas []Pragma) (*sql.DB, error) {
|
|
drv := backendDriver()
|
|
if drv == nil {
|
|
return nil, fmt.Errorf("sqlite: OpenPragma: no %q driver registered", driverName)
|
|
}
|
|
return sql.OpenDB(&pragmaConnector{driver: drv, dsn: dsn, pragmas: pragmas}), nil
|
|
}
|
|
|
|
// pragmaConnector is a database/sql Connector that opens a backend connection
|
|
// and applies pragmas to it before handing it to the pool. Because the pool
|
|
// calls Connect for every new connection, the pragmas hold pool-wide.
|
|
type pragmaConnector struct {
|
|
driver driver.Driver
|
|
dsn string
|
|
pragmas []Pragma
|
|
}
|
|
|
|
func (pc *pragmaConnector) Connect(ctx context.Context) (driver.Conn, error) {
|
|
conn, err := pc.driver.Open(pc.dsn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
execer, ok := conn.(driver.ExecerContext)
|
|
if !ok {
|
|
_ = conn.Close()
|
|
return nil, fmt.Errorf("sqlite: OpenPragma: driver conn %T is not a driver.ExecerContext", conn)
|
|
}
|
|
for _, p := range pc.pragmas {
|
|
stmt := "PRAGMA " + p.Name + " = " + p.Value
|
|
if _, err := execer.ExecContext(ctx, stmt, nil); err != nil {
|
|
_ = conn.Close()
|
|
return nil, fmt.Errorf("sqlite: OpenPragma: apply %q: %w", stmt, err)
|
|
}
|
|
}
|
|
return conn, nil
|
|
}
|
|
|
|
func (pc *pragmaConnector) Driver() driver.Driver { return pc.driver }
|
|
|
|
const driverName = "sqlite"
|
|
|
|
var (
|
|
backendDriverOnce sync.Once
|
|
backendDriverInst driver.Driver
|
|
)
|
|
|
|
// backendDriver returns the driver.Driver registered under the name "sqlite" by
|
|
// this package (mattn under cgo, modernc otherwise). It is resolved once, via a
|
|
// throwaway in-memory sql.DB, so the connector stays tag-neutral — it needs no
|
|
// per-backend driver symbol and works whichever backend init() registered.
|
|
func backendDriver() driver.Driver {
|
|
backendDriverOnce.Do(func() {
|
|
db, err := sql.Open(driverName, "file::memory:")
|
|
if err != nil {
|
|
return
|
|
}
|
|
backendDriverInst = db.Driver()
|
|
_ = db.Close()
|
|
})
|
|
return backendDriverInst
|
|
}
|