package store import ( "context" "fmt" "strings" "git.warky.dev/wdevs/amcs/internal/generatedmodels" "github.com/jackc/pgx/v5" 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, guid, 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 var model generatedmodels.ModelPublicLearnings if err := row.Scan(&model.ID, &model.GUID, &model.CreatedAt, &model.UpdatedAt); err != nil { return thoughttypes.Learning{}, fmt.Errorf("create learning: %w", err) } created.ID = model.ID.Int64() created.GUID = model.GUID.UUID() created.CreatedAt = model.CreatedAt.Time() created.UpdatedAt = model.UpdatedAt.Time() return created, nil } func (db *DB) GetLearning(ctx context.Context, id int64) (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::text[], 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: %d", 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::text[], 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 model generatedmodels.ModelPublicLearnings var tags []string err := row.Scan( &model.ID, &model.Summary, &model.Details, &model.Category, &model.Area, &model.Status, &model.Priority, &model.Confidence, &model.ActionRequired, &model.SourceType, &model.SourceRef, &model.ProjectID, &model.RelatedThoughtID, &model.RelatedSkillID, &model.ReviewedBy, &model.ReviewedAt, &model.DuplicateOfLearningID, &model.SupersedesLearningID, &tags, &model.CreatedAt, &model.UpdatedAt, ) if err != nil { return thoughttypes.Learning{}, err } if tags == nil { tags = []string{} } return learningFromModel(model, tags), 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 }