feat(memory/projectindex): 项目索引分层(索引+描述+章节,Search 定位行+上下文,级联删除)
This commit is contained in:
1 parent
d90ec7bd4f
commit
ef79edd96a
5 files changed
+1010
No files matched your search
@@ -0,0 +1,110 @@
|
||||
package projectindex
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Chapter is a project chapter (layer 3 of the hierarchy): the deep content a
|
||||
// project is split into. ProjectID links it to the owning project. The zero
|
||||
// value is not a valid chapter: AddChapter requires a non-blank title and
|
||||
// content, and generates the id.
|
||||
type Chapter struct {
|
||||
ID string
|
||||
ProjectID string
|
||||
Title string
|
||||
Content string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// AddChapter persists ch under projectID, assigning an id and missing
|
||||
// timestamps. It requires a non-blank projectID, title and content; a blank id
|
||||
// is generated. The project row itself does not have to exist, so chapters may
|
||||
// be added before or after their parent project, but DeleteProject cascades
|
||||
// them away together.
|
||||
func (s *Store) AddChapter(ctx context.Context, projectID string, ch Chapter) error {
|
||||
if strings.TrimSpace(projectID) == "" {
|
||||
return ErrMissingProject
|
||||
}
|
||||
if strings.TrimSpace(ch.Title) == "" {
|
||||
return ErrMissingTitle
|
||||
}
|
||||
if strings.TrimSpace(ch.Content) == "" {
|
||||
return ErrMissingContent
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
if ch.ID == "" {
|
||||
id, err := newID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ch.ID = id
|
||||
}
|
||||
if ch.CreatedAt.IsZero() {
|
||||
ch.CreatedAt = now
|
||||
}
|
||||
if ch.UpdatedAt.IsZero() {
|
||||
ch.UpdatedAt = now
|
||||
}
|
||||
_, err := s.db.ExecContext(ctx, `INSERT INTO project_chapter
|
||||
(id, project_id, title, content, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
ch.ID, projectID, ch.Title, ch.Content,
|
||||
formatTime(ch.CreatedAt), formatTime(ch.UpdatedAt))
|
||||
if err != nil {
|
||||
return fmt.Errorf("projectindex: insert chapter: %w", err)
|
||||
}
|
||||
s.log.Debug("chapter added", "project_id", projectID, "chapter_id", ch.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListChapters returns every chapter for projectID in insertion order. It returns
|
||||
// an empty (non-nil) slice when there are none; a blank projectID returns
|
||||
// ErrMissingProject.
|
||||
func (s *Store) ListChapters(ctx context.Context, projectID string) ([]Chapter, error) {
|
||||
if strings.TrimSpace(projectID) == "" {
|
||||
return nil, ErrMissingProject
|
||||
}
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT id, project_id, title, content,
|
||||
created_at, updated_at FROM project_chapter
|
||||
WHERE project_id = ? ORDER BY created_at ASC, id ASC`, projectID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("projectindex: list chapters: %w", err)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
chapters := make([]Chapter, 0)
|
||||
for rows.Next() {
|
||||
ch, err := scanChapter(rows)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("projectindex: scan chapter: %w", err)
|
||||
}
|
||||
chapters = append(chapters, ch)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("projectindex: iterate chapters: %w", err)
|
||||
}
|
||||
return chapters, nil
|
||||
}
|
||||
|
||||
// scanChapter reads one project_chapter row.
|
||||
func scanChapter(sc rowScanner) (Chapter, error) {
|
||||
var (
|
||||
ch Chapter
|
||||
createdAt, updatedAt string
|
||||
)
|
||||
if err := sc.Scan(&ch.ID, &ch.ProjectID, &ch.Title, &ch.Content, &createdAt, &updatedAt); err != nil {
|
||||
return Chapter{}, err
|
||||
}
|
||||
var err error
|
||||
if ch.CreatedAt, err = parseTime(createdAt); err != nil {
|
||||
return Chapter{}, err
|
||||
}
|
||||
if ch.UpdatedAt, err = parseTime(updatedAt); err != nil {
|
||||
return Chapter{}, err
|
||||
}
|
||||
return ch, nil
|
||||
}
|
||||
Reference in new issue
Block a user