first commit
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
// 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()
|
||||
}
|
||||
Reference in New Issue
Block a user