package store import ( "context" "fmt" "strings" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" thoughttypes "git.warky.dev/wdevs/amcs/internal/types" ) func (db *DB) CreateLearning(ctx context.Context, learning thoughttypes.Learning) (thoughttypes.Learning, error) { row := db.pool.QueryRow(ctx, ` insert into learnings ( summary, details, category, area, status, priority, confidence, action_required, source_type, source_ref, project_id, related_thought_id, related_skill_id, reviewed_by, reviewed_at, duplicate_of_learning_id, supersedes_learning_id, tags ) values ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18 ) returning id, created_at, updated_at `, strings.TrimSpace(learning.Summary), strings.TrimSpace(learning.Details), strings.TrimSpace(learning.Category), strings.TrimSpace(learning.Area), string(learning.Status), string(learning.Priority), string(learning.Confidence), learning.ActionRequired, nullableText(learning.SourceType), nullableText(learning.SourceRef), learning.ProjectID, learning.RelatedThoughtID, learning.RelatedSkillID, nullableTextPtr(learning.ReviewedBy), learning.ReviewedAt, learning.DuplicateOfLearningID, learning.SupersedesLearningID, learning.Tags, ) created := learning if err := row.Scan(&created.ID, &created.CreatedAt, &created.UpdatedAt); err != nil { return thoughttypes.Learning{}, fmt.Errorf("create learning: %w", err) } return created, nil } func (db *DB) GetLearning(ctx context.Context, id uuid.UUID) (thoughttypes.Learning, error) { row := db.pool.QueryRow(ctx, ` select id, summary, details, category, area, status, priority, confidence, action_required, source_type, source_ref, project_id, related_thought_id, related_skill_id, reviewed_by, reviewed_at, duplicate_of_learning_id, supersedes_learning_id, tags, created_at, updated_at from learnings where id = $1 `, id) learning, err := scanLearning(row) if err != nil { if err == pgx.ErrNoRows { return thoughttypes.Learning{}, fmt.Errorf("learning not found: %s", id) } return thoughttypes.Learning{}, fmt.Errorf("get learning: %w", err) } return learning, nil } func (db *DB) ListLearnings(ctx context.Context, filter thoughttypes.LearningFilter) ([]thoughttypes.Learning, error) { args := make([]any, 0, 8) conditions := make([]string, 0, 8) if filter.ProjectID != nil { args = append(args, *filter.ProjectID) conditions = append(conditions, fmt.Sprintf("project_id = $%d", len(args))) } if value := strings.TrimSpace(filter.Category); value != "" { args = append(args, value) conditions = append(conditions, fmt.Sprintf("category = $%d", len(args))) } if value := strings.TrimSpace(filter.Area); value != "" { args = append(args, value) conditions = append(conditions, fmt.Sprintf("area = $%d", len(args))) } if value := strings.TrimSpace(filter.Status); value != "" { args = append(args, value) conditions = append(conditions, fmt.Sprintf("status = $%d", len(args))) } if value := strings.TrimSpace(filter.Priority); value != "" { args = append(args, value) conditions = append(conditions, fmt.Sprintf("priority = $%d", len(args))) } if value := strings.TrimSpace(filter.Tag); value != "" { args = append(args, value) conditions = append(conditions, fmt.Sprintf("$%d = any(tags)", len(args))) } if value := strings.TrimSpace(filter.Query); value != "" { args = append(args, value) conditions = append(conditions, fmt.Sprintf("to_tsvector('simple', summary || ' ' || coalesce(details, '')) @@ websearch_to_tsquery('simple', $%d)", len(args))) } query := ` select id, summary, details, category, area, status, priority, confidence, action_required, source_type, source_ref, project_id, related_thought_id, related_skill_id, reviewed_by, reviewed_at, duplicate_of_learning_id, supersedes_learning_id, tags, created_at, updated_at from learnings ` if len(conditions) > 0 { query += " where " + strings.Join(conditions, " and ") } query += " order by updated_at desc" if filter.Limit > 0 { args = append(args, filter.Limit) query += fmt.Sprintf(" limit $%d", len(args)) } rows, err := db.pool.Query(ctx, query, args...) if err != nil { return nil, fmt.Errorf("list learnings: %w", err) } defer rows.Close() items := make([]thoughttypes.Learning, 0) for rows.Next() { item, err := scanLearning(rows) if err != nil { return nil, fmt.Errorf("scan learning: %w", err) } items = append(items, item) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("iterate learnings: %w", err) } return items, nil } type learningScanner interface { Scan(dest ...any) error } func scanLearning(row learningScanner) (thoughttypes.Learning, error) { var learning thoughttypes.Learning var sourceType pgtype.Text var sourceRef pgtype.Text var reviewedBy pgtype.Text var tags []string err := row.Scan( &learning.ID, &learning.Summary, &learning.Details, &learning.Category, &learning.Area, &learning.Status, &learning.Priority, &learning.Confidence, &learning.ActionRequired, &sourceType, &sourceRef, &learning.ProjectID, &learning.RelatedThoughtID, &learning.RelatedSkillID, &reviewedBy, &learning.ReviewedAt, &learning.DuplicateOfLearningID, &learning.SupersedesLearningID, &tags, &learning.CreatedAt, &learning.UpdatedAt, ) if err != nil { return thoughttypes.Learning{}, err } learning.SourceType = sourceType.String learning.SourceRef = sourceRef.String if reviewedBy.Valid { value := reviewedBy.String learning.ReviewedBy = &value } if tags == nil { learning.Tags = []string{} } else { learning.Tags = tags } return learning, nil } func nullableText(value string) *string { trimmed := strings.TrimSpace(value) if trimmed == "" { return nil } return &trimmed } func nullableTextPtr(value *string) *string { if value == nil { return nil } trimmed := strings.TrimSpace(*value) if trimmed == "" { return nil } return &trimmed }