138 lines
4.3 KiB
Go
138 lines
4.3 KiB
Go
// Package memory implements the persistent memory storage layer for pigo.
|
|
//
|
|
// It is backed by a pure-Go SQLite database (modernc.org/sqlite, no CGO) with
|
|
// an FTS5 full-text index over Markdown memory files. This file provides the
|
|
// Store type together with database opening and idempotent schema migration.
|
|
// Later nodes extend the package with path resolution, reconcile (lazy
|
|
// indexing/pruning) and BM25 search.
|
|
package memory
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
_ "modernc.org/sqlite" // pure-Go SQLite driver, registers the "sqlite" driver
|
|
)
|
|
|
|
// Store is the handle to the memory database. Fields are unexported; later
|
|
// nodes access the underlying *sql.DB via DB() and the configured roots via the
|
|
// package-internal fields.
|
|
type Store struct {
|
|
db *sql.DB
|
|
root string // memory root directory (magic layout: global/projects/sessions)
|
|
ccBase string // optional Claude Code base dir for the "cc" scope; "" disables
|
|
}
|
|
|
|
// Open opens (creating if necessary) the SQLite database at dbPath and runs the
|
|
// idempotent schema migration. The parent directory of dbPath is created with
|
|
// os.MkdirAll before opening. root is the memory root directory and ccBase is
|
|
// the optional Claude Code base directory; both are retained for use by later
|
|
// nodes and are not required to exist here.
|
|
//
|
|
// Calling Open twice on the same file is safe: the migration uses
|
|
// CREATE ... IF NOT EXISTS throughout.
|
|
func Open(dbPath, root, ccBase string) (*Store, error) {
|
|
if dbPath == "" {
|
|
return nil, fmt.Errorf("memory: empty dbPath")
|
|
}
|
|
|
|
// modernc.org/sqlite understands the ":memory:" DSN; only create a parent
|
|
// directory for real file paths.
|
|
if dbPath != ":memory:" {
|
|
if dir := filepath.Dir(dbPath); dir != "" && dir != "." {
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return nil, fmt.Errorf("memory: create db dir %q: %w", dir, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
db, err := sql.Open("sqlite", dbPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("memory: open db %q: %w", dbPath, err)
|
|
}
|
|
|
|
// A single global DB with a single connection: writes are serialized, which
|
|
// matches the low write frequency and avoids SQLITE_BUSY contention.
|
|
db.SetMaxOpenConns(1)
|
|
|
|
if err := db.Ping(); err != nil {
|
|
db.Close()
|
|
return nil, fmt.Errorf("memory: ping db %q: %w", dbPath, err)
|
|
}
|
|
|
|
if err := migrate(db); err != nil {
|
|
db.Close()
|
|
return nil, fmt.Errorf("memory: migrate: %w", err)
|
|
}
|
|
|
|
return &Store{db: db, root: root, ccBase: ccBase}, nil
|
|
}
|
|
|
|
// migrate applies the idempotent schema DDL.
|
|
func migrate(db *sql.DB) error {
|
|
if _, err := db.Exec(schemaDDL); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DB returns the underlying *sql.DB for use by later nodes and tests. It may be
|
|
// nil if the store was not successfully opened.
|
|
func (s *Store) DB() *sql.DB {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
return s.db
|
|
}
|
|
|
|
// Root returns the memory root directory the store was opened with (the magic
|
|
// layout root holding global/projects/sessions). It is the canonical source for
|
|
// the memory root used by checkpoint persistence and context rebuild
|
|
// (<root>/sessions/<id>/checkpoint.md); callers must resolve the memory root
|
|
// through this accessor rather than re-deriving it from another store's dir. It
|
|
// is "" for a nil store.
|
|
func (s *Store) Root() string {
|
|
if s == nil {
|
|
return ""
|
|
}
|
|
return s.root
|
|
}
|
|
|
|
// CountByScope returns the number of indexed memory entries grouped by scope.
|
|
// Scopes with no entries are omitted from the map. It reflects the current
|
|
// contents of memory_index; callers that want fresh counts should Reconcile
|
|
// first. A nil store or nil db yields an empty map.
|
|
func (s *Store) CountByScope() (map[Scope]int, error) {
|
|
out := make(map[Scope]int)
|
|
if s == nil || s.db == nil {
|
|
return out, nil
|
|
}
|
|
rows, err := s.db.Query(`SELECT scope, COUNT(*) FROM memory_index GROUP BY scope`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("memory: count by scope: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var scope string
|
|
var n int
|
|
if err := rows.Scan(&scope, &n); err != nil {
|
|
return nil, fmt.Errorf("memory: scan scope count: %w", err)
|
|
}
|
|
out[Scope(scope)] = n
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("memory: iterate scope counts: %w", err)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// Close closes the underlying database connection.
|
|
func (s *Store) Close() error {
|
|
if s == nil || s.db == nil {
|
|
return nil
|
|
}
|
|
return s.db.Close()
|
|
}
|