Ryanhub - file viewer
filename: memory/memory.go
branch: main
back to repo
package memory

import (
	"database/sql"
	"fmt"
	"strings"
)

// Store owns task CRUD against SQLite.
type Store struct {
	db *sql.DB
}

// NewStore wraps an opened *sql.DB (already migrated).
func NewStore(db *sql.DB) *Store {
	return &Store{db: db}
}

// Close closes the underlying database.
func (s *Store) Close() error {
	if s == nil || s.db == nil {
		return nil
	}
	return s.db.Close()
}

// AddTask inserts a new open task and returns its id.
func (s *Store) AddTask(title string) (int64, error) {
	title = strings.TrimSpace(title)
	if title == "" {
		return 0, fmt.Errorf("task title required")
	}
	res, err := s.db.Exec(`INSERT INTO tasks (title, done) VALUES (?, 0)`, title)
	if err != nil {
		return 0, err
	}
	return res.LastInsertId()
}

// Task is a persisted todo row.
type Task struct {
	ID        int64  `json:"id"`
	Title     string `json:"title"`
	Done      bool   `json:"done"`
	CreatedAt string `json:"created_at"`
}

// Memory is a persisted memory row.
type Memory struct {
	ID        int64  `json:"id"`
	Content   string `json:"content"`
	CreatedAt string `json:"created_at"`
}

// ListOpenTasks returns tasks where done = 0, newest first.
func (s *Store) ListOpenTasks() ([]Task, error) {
	rows, err := s.db.Query(`SELECT id, title, done, created_at FROM tasks WHERE done = 0 ORDER BY id DESC`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []Task
	for rows.Next() {
		var t Task
		var done int
		if err := rows.Scan(&t.ID, &t.Title, &done, &t.CreatedAt); err != nil {
			return nil, err
		}
		t.Done = done != 0
		out = append(out, t)
	}
	return out, rows.Err()
}

// CompleteTask marks a task done by id.
func (s *Store) CompleteTask(id int64) error {
	res, err := s.db.Exec(`UPDATE tasks SET done = 1 WHERE id = ? AND done = 0`, id)
	if err != nil {
		return err
	}
	n, err := res.RowsAffected()
	if err != nil {
		return err
	}
	if n == 0 {
		return fmt.Errorf("no open task with id %d", id)
	}
	return nil
}

// AddMemory inserts one memory and returns its id.
func (s *Store) AddMemory(content string) (int64, error) {
	content = strings.TrimSpace(content)
	if content == "" {
		return 0, fmt.Errorf("memory content required")
	}
	res, err := s.db.Exec(`INSERT INTO memories (content) VALUES (?)`, content)
	if err != nil {
		return 0, err
	}
	return res.LastInsertId()
}

// ListMemories returns newest memories first.
func (s *Store) ListMemories(limit int) ([]Memory, error) {
	if limit <= 0 {
		limit = 20
	}
	if limit > 200 {
		limit = 200
	}
	rows, err := s.db.Query(`SELECT id, content, created_at FROM memories ORDER BY id DESC LIMIT ?`, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []Memory
	for rows.Next() {
		var m Memory
		if err := rows.Scan(&m.ID, &m.Content, &m.CreatedAt); err != nil {
			return nil, err
		}
		out = append(out, m)
	}
	return out, rows.Err()
}

// SearchMemories performs a simple LIKE match over memory content.
func (s *Store) SearchMemories(query string, limit int) ([]Memory, error) {
	q := strings.TrimSpace(query)
	if q == "" {
		return nil, fmt.Errorf("query required")
	}
	if limit <= 0 {
		limit = 20
	}
	if limit > 200 {
		limit = 200
	}
	rows, err := s.db.Query(
		`SELECT id, content, created_at FROM memories WHERE content LIKE ? ORDER BY id DESC LIMIT ?`,
		"%"+q+"%", limit,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []Memory
	for rows.Next() {
		var m Memory
		if err := rows.Scan(&m.ID, &m.Content, &m.CreatedAt); err != nil {
			return nil, err
		}
		out = append(out, m)
	}
	return out, rows.Err()
}

// DeleteMemory removes one memory by id.
func (s *Store) DeleteMemory(id int64) error {
	res, err := s.db.Exec(`DELETE FROM memories WHERE id = ?`, id)
	if err != nil {
		return err
	}
	n, err := res.RowsAffected()
	if err != nil {
		return err
	}
	if n == 0 {
		return fmt.Errorf("no memory with id %d", id)
	}
	return nil
}

// MemoryStats returns count and total content size in characters.
func (s *Store) MemoryStats() (count int64, totalChars int64, err error) {
	row := s.db.QueryRow(`SELECT COUNT(*), COALESCE(SUM(LENGTH(content)), 0) FROM memories`)
	if err := row.Scan(&count, &totalChars); err != nil {
		return 0, 0, err
	}
	return count, totalChars, nil
}

// ClearMemories removes all long-term memory entries.
func (s *Store) ClearMemories() (int64, error) {
	res, err := s.db.Exec(`DELETE FROM memories`)
	if err != nil {
		return 0, err
	}
	return res.RowsAffected()
}

// CompactMemories deduplicates long-term memories by normalized content.
// It keeps the newest entry for each distinct content and deletes older duplicates.
func (s *Store) CompactMemories() (int64, error) {
	rows, err := s.db.Query(`SELECT id, content FROM memories ORDER BY id DESC`)
	if err != nil {
		return 0, err
	}
	defer rows.Close()
	seen := map[string]struct{}{}
	var deleteIDs []int64
	for rows.Next() {
		var id int64
		var content string
		if err := rows.Scan(&id, &content); err != nil {
			return 0, err
		}
		key := strings.ToLower(strings.TrimSpace(content))
		if key == "" {
			deleteIDs = append(deleteIDs, id)
			continue
		}
		if _, ok := seen[key]; ok {
			deleteIDs = append(deleteIDs, id)
			continue
		}
		seen[key] = struct{}{}
	}
	if err := rows.Err(); err != nil {
		return 0, err
	}
	var removed int64
	for _, id := range deleteIDs {
		res, err := s.db.Exec(`DELETE FROM memories WHERE id = ?`, id)
		if err != nil {
			return removed, err
		}
		n, _ := res.RowsAffected()
		removed += n
	}
	return removed, nil
}