mirror of
https://github.com/gtsteffaniak/filebrowser.git
synced 2026-11-14 05:01:59 -05:00
+4







![dependabot[bot]](/avatar/bd5a8d6c673b738d52b0ac42a110045f3f964b3ebfc1d60ea805af743b1dc0e6?size=40)
ad839f92ac
Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Kevin B. <kurami32_kbm@proton.me> Co-authored-by: Swedish Wiking <121635232+Swedish-Wiking@users.noreply.github.com> Co-authored-by: Pyro <pyrodd@gmail.com> Co-authored-by: lps-rocks <10893911+lps-rocks@users.noreply.github.com> Co-authored-by: contributor <foss.contributor@gmail.com> Co-authored-by: Ironic <202444094+YoureIronic@users.noreply.github.com> Co-authored-by: Sarabanga <96509626+Sarabanga@users.noreply.github.com> Co-authored-by: ✿ corey <corey@x64.co> Co-authored-by: Claudio Yanes <me@claudio4.com> Co-authored-by: Aleksandr Baryshnikov <owner@reddec.net> Co-authored-by: SmilingPie <86896922+KevinT3Hu@users.noreply.github.com> Co-authored-by: Maxime Borges <contact@maximeborg.es>
566 lines
21 KiB
Go
566 lines
21 KiB
Go
package sql
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/fileutils"
|
|
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
|
"github.com/gtsteffaniak/go-logger/logger"
|
|
// SQLite driver is imported in driver_cgo.go or driver_nocgo.go based on build tags
|
|
)
|
|
|
|
const (
|
|
// soft heap limit in bytes
|
|
defaultSoftHeapLimitBytes = 32 * 1024 * 1024 // 32MB
|
|
// Pager cache target expressed as number of 4KB pages (negative = pages).
|
|
defaultCacheSizePages = -4096 // ~16MB
|
|
)
|
|
|
|
func init() {
|
|
if SqliteDriver == "sqlite3" {
|
|
logger.Debugf("CGO SQLite driver initialized")
|
|
} else {
|
|
logger.Debugf("Default SQLite driver initialized")
|
|
}
|
|
}
|
|
|
|
// TempDB manages a temporary SQLite database for operations that need
|
|
// to stream large datasets without loading everything into memory.
|
|
// This can be used by any part of the codebase that needs temporary SQLite storage.
|
|
type TempDB struct {
|
|
db *sql.DB
|
|
path string
|
|
mu sync.Mutex
|
|
startTime time.Time
|
|
config *TempDBConfig
|
|
BatchSize int // Exported batch size for bulk operations (from config)
|
|
}
|
|
|
|
// TempDBConfig holds configuration options for temporary SQLite databases.
|
|
type TempDBConfig struct {
|
|
// BatchSize is the number of items to batch for bulk insert.
|
|
// Default: 2500
|
|
BatchSize int
|
|
|
|
// CacheSizeKB is the page cache size in KB. Negative values are in pages.
|
|
// For one-time databases, a smaller cache (e.g., -4096 = ~16MB) is often sufficient.
|
|
// Default: -4096 (approximately 16MB)
|
|
CacheSizeKB int
|
|
|
|
// MmapSize is the memory-mapped I/O size in bytes. Set to 0 to disable mmap.
|
|
// For databases that fit in RAM, set this larger than the expected DB size.
|
|
// Default: 0 (disabled)
|
|
MmapSize int64
|
|
|
|
// Synchronous controls the synchronous mode. OFF is fastest but less safe.
|
|
// For temporary databases, OFF is acceptable.
|
|
// Default: OFF
|
|
Synchronous string
|
|
|
|
// TempStore controls where temporary tables and indices are stored.
|
|
// Valid values: "FILE" (default), "MEMORY", "DEFAULT"
|
|
// Default: FILE (temporary tables stored on disk)
|
|
TempStore string
|
|
|
|
// JournalMode controls the journal mode. WAL is better for concurrent reads/writes.
|
|
// DELETE is faster for write-heavy single-writer workloads.
|
|
// Valid values: "DELETE", "WAL", "TRUNCATE", "PERSIST", "MEMORY", "OFF"
|
|
// Default: WAL
|
|
JournalMode string
|
|
|
|
// LockingMode controls the locking mode. EXCLUSIVE prevents other processes from accessing.
|
|
// Valid values: "NORMAL", "EXCLUSIVE"
|
|
// Default: NORMAL
|
|
LockingMode string
|
|
|
|
// PageSize sets the database page size in bytes. Larger pages improve write performance.
|
|
// Valid values: 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536
|
|
// Default: 4096 (SQLite default)
|
|
PageSize int
|
|
|
|
// AutoVacuum controls automatic vacuuming. NONE disables it for better write performance.
|
|
// Valid values: "NONE", "FULL", "INCREMENTAL"
|
|
// Default: NONE
|
|
AutoVacuum string
|
|
|
|
// SoftHeapLimitBytes, when >0, sets PRAGMA soft_heap_limit so that SQLite
|
|
// proactively frees caches before exceeding this many bytes. Zero disables it.
|
|
SoftHeapLimitBytes int64
|
|
|
|
// HardHeapLimitBytes, when >0, sets PRAGMA hard_heap_limit as a hard cap on SQLite's heap.
|
|
// Operations will fail if they would exceed this limit. Zero disables it.
|
|
HardHeapLimitBytes int64
|
|
|
|
// CacheSpillThreshold sets PRAGMA cache_spill threshold in pages. When cache exceeds both
|
|
// this threshold and cache_size, SQLite will spill dirty pages to disk.
|
|
// Set to 0 to use default behavior (spill enabled but no specific threshold).
|
|
// Default: 500 pages (~2MB) to trigger early spilling and reduce memory usage.
|
|
CacheSpillThreshold int
|
|
|
|
// WalAutocheckpoint sets PRAGMA wal_autocheckpoint in pages. Controls how many pages
|
|
// can accumulate in the WAL file before an automatic checkpoint.
|
|
// Default: 1000 pages (~4MB). Larger values reduce checkpoint frequency.
|
|
WalAutocheckpoint int
|
|
|
|
// JournalSizeLimit sets PRAGMA journal_size_limit in bytes. Limits the maximum size
|
|
// of the WAL journal file. Set to -1 for no limit, 0 for default.
|
|
// Default: 0 (uses SQLite default)
|
|
JournalSizeLimit int64
|
|
|
|
// If empty, a temp file with random suffix will be created.
|
|
// The file will not be deleted on Close() if this is set.
|
|
PersistentFile string
|
|
}
|
|
|
|
// mergeConfig merges the provided config with defaults, returning a new config.
|
|
// If provided config is nil or empty, returns default config.
|
|
func mergeConfig(provided *TempDBConfig) *TempDBConfig {
|
|
defaults := &TempDBConfig{
|
|
BatchSize: 2500,
|
|
CacheSizeKB: defaultCacheSizePages, // ~16MB pager cache
|
|
MmapSize: 0, // Disable mmap to keep RSS predictable
|
|
Synchronous: "OFF",
|
|
TempStore: "FILE", // Default to FILE, not MEMORY
|
|
JournalMode: "WAL", // WAL for better concurrency by default
|
|
LockingMode: "NORMAL",
|
|
PageSize: 4096, // SQLite default
|
|
AutoVacuum: "NONE",
|
|
// Soft heap limit keeps SQLite from retaining excessive pager cache memory.
|
|
SoftHeapLimitBytes: defaultSoftHeapLimitBytes,
|
|
// Hard heap limit provides a hard cap - operations fail if exceeded.
|
|
HardHeapLimitBytes: 0, // Disabled by default, can be set per-database
|
|
// Cache spill threshold triggers early spilling to reduce memory usage.
|
|
CacheSpillThreshold: 500, // ~2MB threshold to trigger early spilling
|
|
}
|
|
|
|
if provided == nil {
|
|
return defaults
|
|
}
|
|
|
|
merged := *defaults // Copy defaults
|
|
|
|
// Override with provided values if they are non-zero/non-empty
|
|
if provided.BatchSize != 0 {
|
|
merged.BatchSize = provided.BatchSize
|
|
}
|
|
if provided.CacheSizeKB != 0 {
|
|
merged.CacheSizeKB = provided.CacheSizeKB
|
|
}
|
|
if provided.MmapSize != 0 {
|
|
merged.MmapSize = provided.MmapSize
|
|
}
|
|
if provided.Synchronous != "" {
|
|
merged.Synchronous = provided.Synchronous
|
|
}
|
|
if provided.TempStore != "" {
|
|
merged.TempStore = provided.TempStore
|
|
}
|
|
if provided.JournalMode != "" {
|
|
merged.JournalMode = provided.JournalMode
|
|
}
|
|
if provided.LockingMode != "" {
|
|
merged.LockingMode = provided.LockingMode
|
|
}
|
|
if provided.PageSize != 0 {
|
|
merged.PageSize = provided.PageSize
|
|
}
|
|
if provided.AutoVacuum != "" {
|
|
merged.AutoVacuum = provided.AutoVacuum
|
|
}
|
|
if provided.HardHeapLimitBytes != 0 {
|
|
merged.HardHeapLimitBytes = provided.HardHeapLimitBytes
|
|
}
|
|
if provided.CacheSpillThreshold != 0 {
|
|
merged.CacheSpillThreshold = provided.CacheSpillThreshold
|
|
}
|
|
// SoftHeapLimitBytes: use provided value if non-zero (default is 32MB, but provided might be different)
|
|
if provided.SoftHeapLimitBytes != 0 {
|
|
merged.SoftHeapLimitBytes = provided.SoftHeapLimitBytes
|
|
}
|
|
// PersistentFile: always copy if non-empty (this determines if file is persistent or temp)
|
|
if provided.PersistentFile != "" {
|
|
merged.PersistentFile = provided.PersistentFile
|
|
}
|
|
|
|
return &merged
|
|
}
|
|
|
|
// NewTempDB creates a new temporary SQLite database.
|
|
// The database is created in the cache directory's sql/ subdirectory.
|
|
// The database will be cleaned up on Close().
|
|
func NewTempDB(id string, config ...*TempDBConfig) (*TempDB, error) {
|
|
startTime := time.Now()
|
|
|
|
// Merge provided config with defaults
|
|
var providedConfig *TempDBConfig
|
|
if len(config) > 0 {
|
|
providedConfig = config[0]
|
|
}
|
|
cfg := mergeConfig(providedConfig)
|
|
|
|
// Create sql subdirectory in the cache directory
|
|
dbDir := filepath.Join(settings.Config.Server.CacheDir, "sql")
|
|
if err := os.MkdirAll(dbDir, fileutils.PermDir); err != nil {
|
|
return nil, fmt.Errorf("failed to create sql directory: %w", err)
|
|
}
|
|
|
|
// Determine database file path
|
|
var tmpPath string
|
|
if cfg.PersistentFile != "" {
|
|
// Use fixed filename for persistent databases
|
|
tmpPath = filepath.Join(dbDir, cfg.PersistentFile)
|
|
// File might already exist (persistent), that's fine
|
|
} else {
|
|
// Create temporary file with random suffix
|
|
tmpFile, err := os.CreateTemp(dbDir, fmt.Sprintf("%s.db", id))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create temp file: %w", err)
|
|
}
|
|
tmpPath = tmpFile.Name()
|
|
tmpFile.Close()
|
|
}
|
|
|
|
// Open SQLite database with basic connection string
|
|
// Driver is selected at compile time: "sqlite3" (CGO) or "sqlite" (pure Go)
|
|
db, err := sql.Open(SqliteDriver, tmpPath)
|
|
if err != nil {
|
|
os.Remove(tmpPath)
|
|
return nil, fmt.Errorf("failed to open SQLite database: %w", err)
|
|
}
|
|
|
|
// Test the connection
|
|
if err := db.Ping(); err != nil {
|
|
db.Close()
|
|
os.Remove(tmpPath)
|
|
return nil, fmt.Errorf("failed to ping SQLite database: %w", err)
|
|
}
|
|
|
|
// Limit connection pool to 1 for SQLite - it's a file-based database and multiple connections
|
|
// can cause locking issues. With busy_timeout, SQLite will queue operations automatically.
|
|
if cfg.LockingMode == "EXCLUSIVE" {
|
|
db.SetMaxOpenConns(1)
|
|
db.SetMaxIdleConns(1)
|
|
}
|
|
|
|
// Calculate cache_size in pages (negative value) to ensure consistent unit handling
|
|
// SQLite's cache_size: positive = KB, negative = pages
|
|
// When using positive KB values, SQLite seems to have issues with cache_spill being less than cache_size
|
|
// So we convert to pages (negative) to ensure both cache_size and cache_spill use the same units
|
|
cacheSizeInPages := cfg.CacheSizeKB
|
|
if cfg.CacheSizeKB > 0 {
|
|
// Convert KB to pages: KB / 4 = pages, then negate to indicate pages
|
|
cacheSizeInPages = -(cfg.CacheSizeKB / 4)
|
|
}
|
|
|
|
basePragmas := []struct {
|
|
sql string
|
|
err string
|
|
}{
|
|
{fmt.Sprintf("PRAGMA journal_mode = %s;", cfg.JournalMode), "failed to set journal_mode"},
|
|
{fmt.Sprintf("PRAGMA cache_size = %d;", cacheSizeInPages), "failed to set cache_size"},
|
|
{fmt.Sprintf("PRAGMA synchronous = %s;", cfg.Synchronous), "failed to set synchronous"},
|
|
{fmt.Sprintf("PRAGMA temp_store = %s;", cfg.TempStore), "failed to set temp_store"},
|
|
{fmt.Sprintf("PRAGMA locking_mode = %s;", cfg.LockingMode), "failed to set locking_mode"},
|
|
{fmt.Sprintf("PRAGMA auto_vacuum = %s;", cfg.AutoVacuum), "failed to set auto_vacuum"},
|
|
}
|
|
|
|
// Memory management PRAGMAs - set early to constrain memory usage
|
|
if cfg.SoftHeapLimitBytes > 0 {
|
|
basePragmas = append([]struct {
|
|
sql string
|
|
err string
|
|
}{{fmt.Sprintf("PRAGMA soft_heap_limit = %d;", cfg.SoftHeapLimitBytes), "failed to set soft_heap_limit"}}, basePragmas...)
|
|
}
|
|
|
|
if cfg.HardHeapLimitBytes > 0 {
|
|
basePragmas = append([]struct {
|
|
sql string
|
|
err string
|
|
}{{fmt.Sprintf("PRAGMA hard_heap_limit = %d;", cfg.HardHeapLimitBytes), "failed to set hard_heap_limit"}}, basePragmas...)
|
|
}
|
|
|
|
// Cache spill will be set later, after all other pragmas, to avoid interaction issues
|
|
|
|
// Page size must be set before any tables are created
|
|
if cfg.PageSize > 0 {
|
|
basePragmas = append([]struct {
|
|
sql string
|
|
err string
|
|
}{{fmt.Sprintf("PRAGMA page_size = %d;", cfg.PageSize), "failed to set page_size"}}, basePragmas...)
|
|
}
|
|
|
|
// Always explicitly set mmap_size to ensure it's disabled (0) or enabled as configured
|
|
// Setting it to 0 explicitly disables memory-mapped I/O, which helps reduce OS page cache usage
|
|
basePragmas = append(basePragmas, struct {
|
|
sql string
|
|
err string
|
|
}{fmt.Sprintf("PRAGMA mmap_size = %d;", cfg.MmapSize), "failed to set mmap_size"})
|
|
|
|
// WAL autocheckpoint - controls checkpoint frequency in WAL mode
|
|
if cfg.WalAutocheckpoint > 0 {
|
|
basePragmas = append(basePragmas, struct {
|
|
sql string
|
|
err string
|
|
}{fmt.Sprintf("PRAGMA wal_autocheckpoint = %d;", cfg.WalAutocheckpoint), "failed to set wal_autocheckpoint"})
|
|
}
|
|
|
|
// Journal size limit - limits WAL file growth
|
|
if cfg.JournalSizeLimit > 0 {
|
|
basePragmas = append(basePragmas, struct {
|
|
sql string
|
|
err string
|
|
}{fmt.Sprintf("PRAGMA journal_size_limit = %d;", cfg.JournalSizeLimit), "failed to set journal_size_limit"})
|
|
} else if cfg.JournalSizeLimit == -1 {
|
|
// -1 means no limit
|
|
basePragmas = append(basePragmas, struct {
|
|
sql string
|
|
err string
|
|
}{"PRAGMA journal_size_limit = -1;", "failed to set journal_size_limit"})
|
|
}
|
|
|
|
for _, pragma := range basePragmas {
|
|
if _, err := db.Exec(pragma.sql); err != nil {
|
|
db.Close()
|
|
os.Remove(tmpPath)
|
|
return nil, fmt.Errorf("%s: %w", pragma.err, err)
|
|
}
|
|
}
|
|
|
|
// Set cache_spill LAST, after all other pragmas are configured
|
|
// SQLite may adjust cache_spill based on internal constraints or formulas
|
|
if cfg.CacheSpillThreshold > 0 {
|
|
// Try setting cache_spill multiple times with different approaches
|
|
// Approach 1: Standard syntax
|
|
if _, err := db.Exec(fmt.Sprintf("PRAGMA cache_spill = %d;", cfg.CacheSpillThreshold)); err != nil {
|
|
logger.Warningf("[SQLITE_CONFIG] Failed to set cache_spill with standard syntax: %v", err)
|
|
}
|
|
|
|
// Immediately verify what value was actually set
|
|
var actualValue int64
|
|
if err := db.QueryRow("PRAGMA cache_spill").Scan(&actualValue); err == nil {
|
|
if actualValue != int64(cfg.CacheSpillThreshold) {
|
|
// SQLite adjusted our value - calculate what percentage it used
|
|
percentage := float64(actualValue) / float64(-cacheSizeInPages) * 100.0
|
|
logger.Warningf("[SQLITE_CONFIG] SQLite adjusted cache_spill from %d to %d pages (%.1f%% of cache_size). This may be due to internal constraints.",
|
|
cfg.CacheSpillThreshold, actualValue, percentage)
|
|
}
|
|
}
|
|
} else {
|
|
// Enable cache_spill with default behavior
|
|
if _, err := db.Exec("PRAGMA cache_spill = 1;"); err != nil {
|
|
logger.Warningf("[SQLITE_CONFIG] Failed to enable cache_spill: %v", err)
|
|
}
|
|
}
|
|
|
|
// Verify PRAGMA settings were applied correctly
|
|
if err := verifyPragmaSettings(db, cfg); err != nil {
|
|
logger.Warningf("Failed to verify PRAGMA settings: %v", err)
|
|
}
|
|
|
|
return &TempDB{
|
|
db: db,
|
|
path: tmpPath,
|
|
startTime: startTime,
|
|
config: cfg,
|
|
BatchSize: cfg.BatchSize, // Export batch size for external packages
|
|
}, nil
|
|
}
|
|
|
|
// DB returns the underlying *sql.DB connection.
|
|
// This allows callers to execute custom SQL queries if needed.
|
|
func (t *TempDB) DB() *sql.DB {
|
|
return t.db
|
|
}
|
|
|
|
// BeginTransaction starts a transaction for bulk operations.
|
|
// The caller must call Commit() or Rollback() on the returned transaction.
|
|
// IMPORTANT: The mutex is acquired here and MUST be released by calling
|
|
// t.mu.Unlock() after commit/rollback to ensure exclusive transaction access.
|
|
func (t *TempDB) BeginTransaction() (*sql.Tx, error) {
|
|
t.mu.Lock()
|
|
// NOTE: Mutex is NOT unlocked here! Caller must unlock after commit/rollback
|
|
return t.db.Begin()
|
|
}
|
|
|
|
// Exec executes a SQL statement that doesn't return rows.
|
|
// This is a convenience method that handles locking.
|
|
func (t *TempDB) Exec(query string, args ...interface{}) (sql.Result, error) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return t.db.Exec(query, args...)
|
|
}
|
|
|
|
// Query executes a query that returns rows.
|
|
// This is a convenience method that handles locking.
|
|
func (t *TempDB) Query(query string, args ...interface{}) (*sql.Rows, error) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return t.db.Query(query, args...)
|
|
}
|
|
|
|
// QueryRow executes a query that is expected to return at most one row.
|
|
// This is a convenience method that handles locking.
|
|
func (t *TempDB) QueryRow(query string, args ...interface{}) *sql.Row {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return t.db.QueryRow(query, args...)
|
|
}
|
|
|
|
// Close closes the database connection and removes the temporary file if it is not persistent.
|
|
func (t *TempDB) Close() error {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
|
|
if t.db != nil {
|
|
if err := t.db.Close(); err != nil {
|
|
// Only remove file if it's not persistent
|
|
if t.config != nil && t.config.PersistentFile == "" {
|
|
os.Remove(t.path)
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Only remove file if it's not persistent
|
|
if t.config != nil && t.config.PersistentFile == "" {
|
|
return os.Remove(t.path)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// verifyPragmaSettings reads back PRAGMA values to verify they were applied correctly
|
|
func verifyPragmaSettings(db *sql.DB, cfg *TempDBConfig) error {
|
|
var cacheSize, mmapSize, softHeapLimit, hardHeapLimit, cacheSpill int64
|
|
var journalMode, synchronous, tempStore, lockingMode, autoVacuum string
|
|
|
|
// Read back PRAGMA values
|
|
if err := db.QueryRow("PRAGMA cache_size").Scan(&cacheSize); err != nil {
|
|
return fmt.Errorf("failed to read cache_size: %w", err)
|
|
}
|
|
if err := db.QueryRow("PRAGMA mmap_size").Scan(&mmapSize); err != nil {
|
|
return fmt.Errorf("failed to read mmap_size: %w", err)
|
|
}
|
|
if err := db.QueryRow("PRAGMA soft_heap_limit").Scan(&softHeapLimit); err != nil {
|
|
return fmt.Errorf("failed to read soft_heap_limit: %w", err)
|
|
}
|
|
if err := db.QueryRow("PRAGMA hard_heap_limit").Scan(&hardHeapLimit); err != nil {
|
|
return fmt.Errorf("failed to read hard_heap_limit: %w", err)
|
|
}
|
|
if err := db.QueryRow("PRAGMA cache_spill").Scan(&cacheSpill); err != nil {
|
|
return fmt.Errorf("failed to read cache_spill: %w", err)
|
|
}
|
|
if err := db.QueryRow("PRAGMA journal_mode").Scan(&journalMode); err != nil {
|
|
return fmt.Errorf("failed to read journal_mode: %w", err)
|
|
}
|
|
if err := db.QueryRow("PRAGMA synchronous").Scan(&synchronous); err != nil {
|
|
return fmt.Errorf("failed to read synchronous: %w", err)
|
|
}
|
|
if err := db.QueryRow("PRAGMA temp_store").Scan(&tempStore); err != nil {
|
|
return fmt.Errorf("failed to read temp_store: %w", err)
|
|
}
|
|
if err := db.QueryRow("PRAGMA locking_mode").Scan(&lockingMode); err != nil {
|
|
return fmt.Errorf("failed to read locking_mode: %w", err)
|
|
}
|
|
if err := db.QueryRow("PRAGMA auto_vacuum").Scan(&autoVacuum); err != nil {
|
|
return fmt.Errorf("failed to read auto_vacuum: %w", err)
|
|
}
|
|
|
|
// Convert cache_size from pages to KB (negative = pages, positive = KB)
|
|
cacheSizeKB := cacheSize
|
|
if cacheSize < 0 {
|
|
cacheSizeKB = -cacheSize * 4 // pages * 4KB per page
|
|
}
|
|
|
|
// Expected cache_spill value in pages (cache_spill is always specified and returned in pages)
|
|
expectedCacheSpill := int64(cfg.CacheSpillThreshold)
|
|
|
|
// Convert numeric PRAGMA values to human-readable strings
|
|
tempStoreMap := map[string]string{"0": "DEFAULT", "1": "FILE", "2": "MEMORY"}
|
|
synchronousMap := map[string]string{"0": "OFF", "1": "NORMAL", "2": "FULL", "3": "EXTRA"}
|
|
autoVacuumMap := map[string]string{"0": "NONE", "1": "FULL", "2": "INCREMENTAL"}
|
|
|
|
tempStoreStr := tempStoreMap[tempStore]
|
|
if tempStoreStr == "" {
|
|
tempStoreStr = tempStore
|
|
}
|
|
synchronousStr := synchronousMap[synchronous]
|
|
if synchronousStr == "" {
|
|
synchronousStr = synchronous
|
|
}
|
|
autoVacuumStr := autoVacuumMap[autoVacuum]
|
|
if autoVacuumStr == "" {
|
|
autoVacuumStr = autoVacuum
|
|
}
|
|
|
|
// Log all settings
|
|
logger.Debugf("[SQLITE_CONFIG] Database PRAGMA settings verified:")
|
|
logger.Debugf(" cache_size : %d KB", cacheSizeKB)
|
|
logger.Debugf(" mmap_size : %d bytes", mmapSize)
|
|
logger.Debugf(" soft_heap_limit : %d bytes", softHeapLimit)
|
|
logger.Debugf(" hard_heap_limit : %d bytes", hardHeapLimit)
|
|
logger.Debugf(" cache_spill : %d pages", cacheSpill)
|
|
logger.Debugf(" journal_mode : %s", journalMode)
|
|
logger.Debugf(" synchronous : %s", synchronousStr)
|
|
logger.Debugf(" temp_store : %s", tempStoreStr)
|
|
logger.Debugf(" locking_mode : %s", lockingMode)
|
|
logger.Debugf(" auto_vacuum : %s", autoVacuumStr)
|
|
|
|
// Verify critical settings match
|
|
if cfg.CacheSizeKB != 0 {
|
|
expectedCache := int64(cfg.CacheSizeKB)
|
|
if expectedCache < 0 {
|
|
expectedCache = -expectedCache * 4 // Convert pages to KB
|
|
}
|
|
if cacheSizeKB != expectedCache {
|
|
logger.Warningf("[SQLITE_CONFIG] cache_size mismatch: got %d KB, expected %d KB", cacheSizeKB, expectedCache)
|
|
}
|
|
}
|
|
if cfg.MmapSize > 0 && mmapSize != cfg.MmapSize {
|
|
logger.Warningf("[SQLITE_CONFIG] mmap_size mismatch: got %d bytes, expected %d bytes", mmapSize, cfg.MmapSize)
|
|
}
|
|
if cfg.SoftHeapLimitBytes > 0 && softHeapLimit != cfg.SoftHeapLimitBytes {
|
|
logger.Warningf("[SQLITE_CONFIG] soft_heap_limit mismatch: got %d bytes, expected %d bytes", softHeapLimit, cfg.SoftHeapLimitBytes)
|
|
}
|
|
if cfg.HardHeapLimitBytes > 0 && hardHeapLimit != cfg.HardHeapLimitBytes {
|
|
logger.Warningf("[SQLITE_CONFIG] hard_heap_limit mismatch: got %d bytes, expected %d bytes", hardHeapLimit, cfg.HardHeapLimitBytes)
|
|
}
|
|
// Verify cache_spill matches expected value (accounting for unit conversion)
|
|
if cfg.CacheSpillThreshold > 0 && cacheSpill != expectedCacheSpill {
|
|
logger.Warningf("[SQLITE_CONFIG] cache_spill mismatch: got %d pages, expected %d pages (from threshold %d pages)", cacheSpill, expectedCacheSpill, cfg.CacheSpillThreshold)
|
|
// If cache_spill equals cache_size in pages, it means SQLite ignored our setting
|
|
var cacheSizePages int64
|
|
if cfg.CacheSizeKB > 0 {
|
|
cacheSizePages = int64(cfg.CacheSizeKB) / 4 // KB to pages
|
|
} else {
|
|
cacheSizePages = -int64(cfg.CacheSizeKB) // Already in pages (negative)
|
|
}
|
|
if cacheSpill == cacheSizePages {
|
|
logger.Warningf("[SQLITE_CONFIG] cache_spill equals cache_size (%d pages), indicating SQLite may have ignored our setting", cacheSizePages)
|
|
}
|
|
}
|
|
|
|
// Verify string-based PRAGMA settings match (using converted values)
|
|
if synchronousStr != cfg.Synchronous {
|
|
logger.Warningf("[SQLITE_CONFIG] synchronous mismatch: got %s, expected %s", synchronousStr, cfg.Synchronous)
|
|
}
|
|
if tempStoreStr != cfg.TempStore {
|
|
logger.Warningf("[SQLITE_CONFIG] temp_store mismatch: got %s, expected %s", tempStoreStr, cfg.TempStore)
|
|
}
|
|
if autoVacuumStr != cfg.AutoVacuum {
|
|
logger.Warningf("[SQLITE_CONFIG] auto_vacuum mismatch: got %s, expected %s", autoVacuumStr, cfg.AutoVacuum)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Path returns the path to the temporary database file.
|
|
// This is useful for debugging or if you need to inspect the database.
|
|
func (t *TempDB) Path() string {
|
|
return t.path
|
|
}
|