package cache
import (
"context"
"crypto/rand"
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"time"
_ "modernc.org/sqlite"
)
type SQLiteStore struct {
db *sql.DB
useFTS bool
forceNoFTS bool
cachePath string
lockPath string
cacheRef string
ephemeralLock bool
memoryAnchorDB *sql.DB
memoryAnchorConn *sql.Conn
closeOnce sync.Once
closeErr error
}
type LockHandle struct {
file *os.File
path string
owner WriterOwner
}
type WriterOwner struct {
Operation string `json:"operation"`
RepoID string `json:"repo_id,omitempty"`
StartedAt time.Time `json:"started_at"`
PID int `json:"pid"`
CacheRef string `json:"cache_ref,omitempty"`
CachePath string `json:"cache_path,omitempty"`
}
type WriterLease struct {
lock *LockHandle
Owner WriterOwner
}
type WriterRequest struct {
Operation string
RepoID string
LockPath string
}
func NewSQLiteStore(ctx context.Context, dataSourceName string) (*SQLiteStore, error) {
return newSQLiteStore(ctx, dataSourceName, false)
}
func newSQLiteStore(ctx context.Context, dataSourceName string, forceNoFTS bool) (*SQLiteStore, error) {
cachePath, err := cachePathForDataSource(dataSourceName)
if err != nil {
return nil, err
}
poolDataSource := cachePath
var memoryIdentity [16]byte
if dataSourceName == ":memory:" {
if _, err := rand.Read(memoryIdentity[:]); err != nil {
return nil, err
}
poolDataSource = fmt.Sprintf("file:gitcode-mcp-memory-%x?mode=memory&cache=shared&_pragma=foreign_keys(1)&_pragma=busy_timeout(5000)", memoryIdentity)
}
db, err := sql.Open("sqlite", poolDataSource)
if err != nil {
return nil, err
}
db.SetMaxOpenConns(8)
db.SetMaxIdleConns(4)
if dataSourceName == ":memory:" {
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1)
}
lockPath := writerLockPath(cachePath)
store := &SQLiteStore{db: db, forceNoFTS: forceNoFTS, cachePath: cachePath, lockPath: lockPath}
initialized := false
defer func() {
if !initialized {
_ = store.Close()
}
}()
if dataSourceName == ":memory:" {
store.lockPath = filepath.Join(os.TempDir(), fmt.Sprintf("gitcode-mcp-memory-writer-%x.lock", memoryIdentity))
store.ephemeralLock = true
store.memoryAnchorDB, err = sql.Open("sqlite", poolDataSource)
if err != nil {
return nil, err
}
store.memoryAnchorConn, err = store.memoryAnchorDB.Conn(ctx)
if err != nil {
return nil, err
}
}
if _, err := db.ExecContext(ctx, "PRAGMA foreign_keys = ON"); err != nil {
return nil, err
}
if _, err := db.ExecContext(ctx, "PRAGMA busy_timeout = 5000"); err != nil {
return nil, err
}
if dataSourceName != ":memory:" {
if _, err := db.ExecContext(ctx, "PRAGMA journal_mode = WAL"); err != nil {
return nil, err
}
}
useFTS := !forceNoFTS && detectFTS5(ctx, db)
store.useFTS = useFTS
if dataSourceName == ":memory:" {
if err := runMigrations(ctx, db, useFTS); err != nil {
return nil, err
}
store.loadCacheRef(ctx)
initialized = true
return store, nil
}
compat, err := CheckVersionCompatibility(ctx, db)
if err != nil {
return nil, err
}
if !compat.PermitWrites {
return nil, &SchemaVersionError{Compat: compat}
}
if compat.DetectedVersion == compat.ExpectedVersion {
store.loadCacheRef(ctx)
initialized = true
return store, nil
}
lease, err := store.AcquireWriter(ctx, WriterRequest{Operation: "migration"})
if err != nil {
return nil, err
}
if err := runMigrations(ctx, db, useFTS); err != nil {
_ = store.ReleaseWriter(context.Background(), lease)
return nil, err
}
if err := store.ReleaseWriter(context.Background(), lease); err != nil {
return nil, err
}
store.loadCacheRef(ctx)
initialized = true
return store, nil
}
func NewSQLiteReadOnlyStore(ctx context.Context, dataSourceName string) (*SQLiteStore, error) {
if _, err := os.Stat(dataSourceName); err != nil {
return nil, fmt.Errorf("cache: cannot open read-only store: %w", err)
}
dsn, err := sqliteReadOnlyDSN(dataSourceName)
if err != nil {
return nil, err
}
db, err := sql.Open("sqlite", dsn)
if err != nil {
return nil, err
}
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1)
if _, err := db.ExecContext(ctx, "PRAGMA foreign_keys = ON"); err != nil {
_ = db.Close()
return nil, err
}
if _, err := db.ExecContext(ctx, "PRAGMA busy_timeout = 5000"); err != nil {
_ = db.Close()
return nil, err
}
compat, err := CheckVersionCompatibility(ctx, db)
if err != nil {
_ = db.Close()
return nil, err
}
if !compat.Compatible {
_ = db.Close()
return nil, &SchemaVersionError{Compat: compat}
}
useFTS := detectFTS5(ctx, db)
return &SQLiteStore{db: db, useFTS: useFTS, forceNoFTS: false, cachePath: dataSourceName, lockPath: ""}, nil
}
func sqliteReadOnlyDSN(dataSourceName string) (string, error) {
return sqliteFileDSN(dataSourceName, false)
}
func sqliteImmutableReadOnlyDSN(dataSourceName string) (string, error) {
return sqliteFileDSN(dataSourceName, true)
}
func sqliteFileDSN(dataSourceName string, immutable bool) (string, error) {
absolutePath, err := filepath.Abs(dataSourceName)
if err != nil {
return "", fmt.Errorf("cache: resolve read-only store path: %w", err)
}
uriPath := filepath.ToSlash(absolutePath)
if !strings.HasPrefix(uriPath, "/") {
uriPath = "/" + uriPath
}
uri := url.URL{Scheme: "file", Path: uriPath}
query := uri.Query()
query.Set("mode", "ro")
if immutable {
query.Set("immutable", "1")
}
uri.RawQuery = query.Encode()
return uri.String(), nil
}
func (s *SQLiteStore) SchemaVersion(ctx context.Context) (int, error) {
return schemaVersion(ctx, s.db)
}
func NewInMemorySQLiteStore(ctx context.Context) (*SQLiteStore, error) {
return NewSQLiteStore(ctx, ":memory:")
}
func cachePathForDataSource(dataSourceName string) (string, error) {
if dataSourceName == ":memory:" || strings.HasPrefix(dataSourceName, "file:") {
return dataSourceName, nil
}
abs, err := filepath.Abs(dataSourceName)
if err != nil {
return "", writerAuthorityResolutionError(err)
}
for depth := 0; depth < 32; depth++ {
physical, err := filepath.EvalSymlinks(abs)
if err == nil {
return physical, nil
}
if !os.IsNotExist(err) {
return "", writerAuthorityResolutionError(err)
}
info, statErr := os.Lstat(abs)
if statErr == nil && info.Mode()&os.ModeSymlink != 0 {
target, err := os.Readlink(abs)
if err != nil {
return "", writerAuthorityResolutionError(err)
}
if !filepath.IsAbs(target) {
target = filepath.Join(filepath.Dir(abs), target)
}
abs = filepath.Clean(target)
continue
}
if statErr != nil && !os.IsNotExist(statErr) {
return "", writerAuthorityResolutionError(statErr)
}
parent, err := filepath.EvalSymlinks(filepath.Dir(abs))
if err != nil {
return "", writerAuthorityResolutionError(err)
}
return filepath.Join(parent, filepath.Base(abs)), nil
}
return "", writerAuthorityResolutionError(nil)
}
func writerLockPath(cachePath string) string {
if cachePath == "" || cachePath == ":memory:" || strings.HasPrefix(cachePath, "file:") {
return filepath.Join(os.TempDir(), "gitcode-mcp-cache-writer.lock")
}
return cachePath + ".writer.lock"
}
func (s *SQLiteStore) loadCacheRef(ctx context.Context) {
identity, err := s.CacheIdentity(ctx)
if err != nil {
return
}
s.cacheRef = strings.TrimSpace(identity.UUID)
}
func (s *SQLiteStore) Close() error {
s.closeOnce.Do(func() {
s.closeErr = s.db.Close()
if s.memoryAnchorConn != nil {
s.closeErr = errors.Join(s.closeErr, s.memoryAnchorConn.Close())
}
if s.memoryAnchorDB != nil {
s.closeErr = errors.Join(s.closeErr, s.memoryAnchorDB.Close())
}
if s.ephemeralLock {
_ = os.Remove(s.lockPath)
}
})
return s.closeErr
}
func marshalJSON(v any) (string, error) {
b, err := json.Marshal(v)
if err != nil {
return "", err
}
return string(b), nil
}
func unmarshalJSON[T any](raw string) (T, error) {
var v T
if raw == "" {
return v, nil
}
if err := json.Unmarshal([]byte(raw), &v); err != nil {
return v, err
}
return v, nil
}
func execTx(ctx context.Context, tx *sql.Tx, query string, args ...any) error {
_, err := tx.ExecContext(ctx, query, args...)
return err
}
func txRollbackOnError(tx *sql.Tx, errp *error) {
if *errp != nil {
_ = tx.Rollback()
}
}
func (s *SQLiteStore) IntegrityCheck(ctx context.Context) error {
var result string
if err := s.db.QueryRowContext(ctx, "PRAGMA integrity_check").Scan(&result); err != nil {
return err
}
if result != "ok" {
return ErrCacheCorruption{Path: "sqlite", Detail: result}
}
return nil
}
func (s *SQLiteStore) Checkpoint(ctx context.Context, reason string) error {
_ = reason
var busy, logFrames, checkpointed int
if err := s.db.QueryRowContext(ctx, "PRAGMA wal_checkpoint(PASSIVE)").Scan(&busy, &logFrames, &checkpointed); err != nil {
return err
}
if busy != 0 {
return ErrLockContention{Path: s.cachePath, HolderHint: "sqlite checkpoint busy"}
}
return nil
}
func notFoundErr(thing, id string) error {
return fmt.Errorf("%w: %s %s", ErrNotFound, thing, id)
}