package db import ( "context" "database/sql" "encoding/json" "strings" "time" "github.com/gooseek/backend/internal/computer" ) type ComputerMemoryRepo struct { db *sql.DB } func NewComputerMemoryRepo(db *sql.DB) *ComputerMemoryRepo { return &ComputerMemoryRepo{db: db} } func (r *ComputerMemoryRepo) Migrate() error { query := ` CREATE TABLE IF NOT EXISTS computer_memory ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL, task_id UUID, key VARCHAR(255) NOT NULL, value JSONB NOT NULL, type VARCHAR(50), tags TEXT[], created_at TIMESTAMPTZ DEFAULT NOW(), expires_at TIMESTAMPTZ ); CREATE INDEX IF NOT EXISTS idx_computer_memory_user_id ON computer_memory(user_id); CREATE INDEX IF NOT EXISTS idx_computer_memory_task_id ON computer_memory(task_id); CREATE INDEX IF NOT EXISTS idx_computer_memory_type ON computer_memory(type); CREATE INDEX IF NOT EXISTS idx_computer_memory_expires ON computer_memory(expires_at) WHERE expires_at IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_computer_memory_key ON computer_memory(key); ` _, err := r.db.Exec(query) return err } func (r *ComputerMemoryRepo) Store(ctx context.Context, entry *computer.MemoryEntry) error { valueJSON, err := json.Marshal(entry.Value) if err != nil { return err } query := ` INSERT INTO computer_memory (id, user_id, task_id, key, value, type, tags, created_at, expires_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ON CONFLICT (id) DO UPDATE SET value = EXCLUDED.value, type = EXCLUDED.type, tags = EXCLUDED.tags, expires_at = EXCLUDED.expires_at ` var taskID interface{} if entry.TaskID != "" { taskID = entry.TaskID } _, err = r.db.ExecContext(ctx, query, entry.ID, entry.UserID, taskID, entry.Key, valueJSON, entry.Type, entry.Tags, entry.CreatedAt, entry.ExpiresAt, ) return err } func (r *ComputerMemoryRepo) GetByUser(ctx context.Context, userID string, limit int) ([]computer.MemoryEntry, error) { query := ` SELECT id, user_id, task_id, key, value, type, tags, created_at, expires_at FROM computer_memory WHERE user_id = $1 AND (expires_at IS NULL OR expires_at > NOW()) ORDER BY created_at DESC LIMIT $2 ` rows, err := r.db.QueryContext(ctx, query, userID, limit) if err != nil { return nil, err } defer rows.Close() return r.scanEntries(rows) } func (r *ComputerMemoryRepo) GetByTask(ctx context.Context, taskID string) ([]computer.MemoryEntry, error) { query := ` SELECT id, user_id, task_id, key, value, type, tags, created_at, expires_at FROM computer_memory WHERE task_id = $1 AND (expires_at IS NULL OR expires_at > NOW()) ORDER BY created_at ASC ` rows, err := r.db.QueryContext(ctx, query, taskID) if err != nil { return nil, err } defer rows.Close() return r.scanEntries(rows) } func (r *ComputerMemoryRepo) Search(ctx context.Context, userID, query string, limit int) ([]computer.MemoryEntry, error) { searchTerms := strings.Fields(strings.ToLower(query)) if len(searchTerms) == 0 { return r.GetByUser(ctx, userID, limit) } likePatterns := make([]string, len(searchTerms)) args := make([]interface{}, len(searchTerms)+2) args[0] = userID for i, term := range searchTerms { likePatterns[i] = "%" + term + "%" args[i+1] = likePatterns[i] } args[len(args)-1] = limit var conditions []string for i := range searchTerms { conditions = append(conditions, "(LOWER(key) LIKE $"+string(rune('2'+i))+" OR LOWER(value::text) LIKE $"+string(rune('2'+i))+")") } sqlQuery := ` SELECT id, user_id, task_id, key, value, type, tags, created_at, expires_at FROM computer_memory WHERE user_id = $1 AND (expires_at IS NULL OR expires_at > NOW()) AND (` + strings.Join(conditions, " OR ") + `) ORDER BY created_at DESC LIMIT $` + string(rune('2'+len(searchTerms))) rows, err := r.db.QueryContext(ctx, sqlQuery, args...) if err != nil { return r.GetByUser(ctx, userID, limit) } defer rows.Close() return r.scanEntries(rows) } func (r *ComputerMemoryRepo) GetByType(ctx context.Context, userID, memType string, limit int) ([]computer.MemoryEntry, error) { query := ` SELECT id, user_id, task_id, key, value, type, tags, created_at, expires_at FROM computer_memory WHERE user_id = $1 AND type = $2 AND (expires_at IS NULL OR expires_at > NOW()) ORDER BY created_at DESC LIMIT $3 ` rows, err := r.db.QueryContext(ctx, query, userID, memType, limit) if err != nil { return nil, err } defer rows.Close() return r.scanEntries(rows) } func (r *ComputerMemoryRepo) GetByKey(ctx context.Context, userID, key string) (*computer.MemoryEntry, error) { query := ` SELECT id, user_id, task_id, key, value, type, tags, created_at, expires_at FROM computer_memory WHERE user_id = $1 AND key = $2 AND (expires_at IS NULL OR expires_at > NOW()) ORDER BY created_at DESC LIMIT 1 ` var entry computer.MemoryEntry var valueJSON []byte var taskID sql.NullString var expiresAt sql.NullTime var tags []string err := r.db.QueryRowContext(ctx, query, userID, key).Scan( &entry.ID, &entry.UserID, &taskID, &entry.Key, &valueJSON, &entry.Type, &tags, &entry.CreatedAt, &expiresAt, ) if err != nil { return nil, err } if taskID.Valid { entry.TaskID = taskID.String } if expiresAt.Valid { entry.ExpiresAt = &expiresAt.Time } entry.Tags = tags json.Unmarshal(valueJSON, &entry.Value) return &entry, nil } func (r *ComputerMemoryRepo) Delete(ctx context.Context, id string) error { query := `DELETE FROM computer_memory WHERE id = $1` _, err := r.db.ExecContext(ctx, query, id) return err } func (r *ComputerMemoryRepo) DeleteByUser(ctx context.Context, userID string) error { query := `DELETE FROM computer_memory WHERE user_id = $1` _, err := r.db.ExecContext(ctx, query, userID) return err } func (r *ComputerMemoryRepo) DeleteByTask(ctx context.Context, taskID string) error { query := `DELETE FROM computer_memory WHERE task_id = $1` _, err := r.db.ExecContext(ctx, query, taskID) return err } func (r *ComputerMemoryRepo) DeleteExpired(ctx context.Context) (int64, error) { query := `DELETE FROM computer_memory WHERE expires_at IS NOT NULL AND expires_at < NOW()` result, err := r.db.ExecContext(ctx, query) if err != nil { return 0, err } return result.RowsAffected() } func (r *ComputerMemoryRepo) scanEntries(rows *sql.Rows) ([]computer.MemoryEntry, error) { var entries []computer.MemoryEntry for rows.Next() { var entry computer.MemoryEntry var valueJSON []byte var taskID sql.NullString var expiresAt sql.NullTime var tags []string err := rows.Scan( &entry.ID, &entry.UserID, &taskID, &entry.Key, &valueJSON, &entry.Type, &tags, &entry.CreatedAt, &expiresAt, ) if err != nil { continue } if taskID.Valid { entry.TaskID = taskID.String } if expiresAt.Valid { entry.ExpiresAt = &expiresAt.Time } entry.Tags = tags json.Unmarshal(valueJSON, &entry.Value) entries = append(entries, entry) } return entries, nil } func (r *ComputerMemoryRepo) Count(ctx context.Context, userID string) (int64, error) { query := ` SELECT COUNT(*) FROM computer_memory WHERE user_id = $1 AND (expires_at IS NULL OR expires_at > NOW()) ` var count int64 err := r.db.QueryRowContext(ctx, query, userID).Scan(&count) return count, err } func (r *ComputerMemoryRepo) UpdateExpiry(ctx context.Context, id string, expiresAt time.Time) error { query := `UPDATE computer_memory SET expires_at = $1 WHERE id = $2` _, err := r.db.ExecContext(ctx, query, expiresAt, id) return err }