ModifyMessageLabels applies a delta — add then remove — to a message's labels[] in a single atomic statement. Returns the updated labels (deduplicated, sorted) so the caller can echo them back in the response without a second round-trip. Inputs are assumed already normalized (lowercased, charset-va
(ctx context.Context, messageID, agentID string, add, remove []string)
| 2629 | // The whole thing runs as one UPDATE so a concurrent PATCH from a |
| 2630 | // second client can't observe a partial state. |
| 2631 | func (s *Store) ModifyMessageLabels(ctx context.Context, messageID, agentID string, add, remove []string) ([]string, error) { |
| 2632 | // Pre-check the post-add length against the cap. Done as a |
| 2633 | // dedicated SELECT-then-UPDATE so we can return a specific error |
| 2634 | // rather than a generic constraint violation — the handler maps |
| 2635 | // ErrLabelLimitExceeded to 400 with a useful message. |
| 2636 | tx, err := s.pool.Begin(ctx) |
| 2637 | if err != nil { |
| 2638 | return nil, err |
| 2639 | } |
| 2640 | defer tx.Rollback(ctx) |
| 2641 | |
| 2642 | var current []string |
| 2643 | err = tx.QueryRow(ctx, |
| 2644 | `SELECT labels FROM messages WHERE id = $1 AND agent_id = $2 AND expires_at > now() AND NOT (direction = 'inbound' AND status IN (`+heldInboundStatuses+`)) FOR UPDATE`, |
| 2645 | messageID, agentID, |
| 2646 | ).Scan(¤t) |
| 2647 | if err != nil { |
| 2648 | if errors.Is(err, pgx.ErrNoRows) { |
| 2649 | return nil, ErrMessageNotFound |
| 2650 | } |
| 2651 | return nil, err |
| 2652 | } |
| 2653 | |
| 2654 | // Apply the delta in-memory so the cap check is exact. The set |
| 2655 | // semantics here mirror what the SQL UPDATE below does: |
| 2656 | // labels' = (labels ∪ add) \ remove. |
| 2657 | labelSet := map[string]struct{}{} |
| 2658 | for _, l := range current { |
| 2659 | labelSet[l] = struct{}{} |
| 2660 | } |
| 2661 | for _, l := range add { |
| 2662 | labelSet[l] = struct{}{} |
| 2663 | } |
| 2664 | for _, l := range remove { |
| 2665 | delete(labelSet, l) |
| 2666 | } |
| 2667 | if len(labelSet) > MaxLabelsPerMessage { |
| 2668 | return nil, ErrLabelLimitExceeded |
| 2669 | } |
| 2670 | |
| 2671 | final := make([]string, 0, len(labelSet)) |
| 2672 | for l := range labelSet { |
| 2673 | final = append(final, l) |
| 2674 | } |
| 2675 | sort.Strings(final) |
| 2676 | |
| 2677 | if _, err := tx.Exec(ctx, |
| 2678 | `UPDATE messages SET labels = $1 WHERE id = $2 AND agent_id = $3 AND NOT (direction = 'inbound' AND status IN (`+heldInboundStatuses+`))`, |
| 2679 | final, messageID, agentID, |
| 2680 | ); err != nil { |
| 2681 | return nil, err |
| 2682 | } |
| 2683 | if err := tx.Commit(ctx); err != nil { |
| 2684 | return nil, err |
| 2685 | } |
| 2686 | if final == nil { |
| 2687 | final = []string{} |
| 2688 | } |