mattermost-community/focalboard · error

store addUpdateCategoryBoard: failed to upsert user-board-ca

Error message

store addUpdateCategoryBoard: failed to upsert user-board-category userID: %s, categoryID: %s, board_count: %d, error: %w

What it means

Error returned by SQLStore.addUpdateCategoryBoard (server/services/store/sqlstore/category_boards.go:110) when the upsert into the category_boards table fails at query.Exec(). The store uses INSERT ... ON CONFLICT (user_id, board_id) DO UPDATE to reassign boards to a category; any database-level failure of that statement is wrapped with userID, categoryID, and the number of boards being moved.

Source

Thrown at server/services/store/sqlstore/category_boards.go:110

			0,
			false,
		)
	}

	if s.dbType == model.MysqlDBType {
		query = query.Suffix(
			"ON DUPLICATE KEY UPDATE category_id = ?",
			categoryID,
		)
	} else {
		query = query.Suffix(
			`ON CONFLICT (user_id, board_id)
			 DO UPDATE SET category_id = EXCLUDED.category_id, update_at = EXCLUDED.update_at`,
		)
	}

	if _, err := query.Exec(); err != nil {
		return fmt.Errorf(
			"store addUpdateCategoryBoard: failed to upsert user-board-category userID: %s, categoryID: %s, board_count: %d, error: %w",
			userID, categoryID, len(boardIDs), err,
		)
	}

	return nil
}

func (s *SQLStore) categoryBoardsFromRows(rows *sql.Rows) ([]model.CategoryBoardMetadata, error) {
	metadata := []model.CategoryBoardMetadata{}

	for rows.Next() {
		datum := model.CategoryBoardMetadata{}
		err := rows.Scan(&datum.BoardID, &datum.Hidden)

		if err != nil {
			s.logger.Error("categoryBoardsFromRows row scan error", mlog.Err(err))
			return nil, err

View on GitHub (pinned to a84bbb65e3)

Solutions

  1. Read the wrapped %w error to get the exact DB cause (FK violation, deadlock, 'ON CONFLICT DO UPDATE command cannot affect row a second time').
  2. Deduplicate boardIDs before calling AddUpdateCategoryBoard.
  3. Verify the categoryID exists in the categories table before the batch upsert.
  4. Retry on transient errors (deadlock/serialization) with backoff; consider chunking large boardID batches.
  5. Run schema migrations so category_boards matches the expected columns and constraints.

Example fix

// before
boardIDs := []string{"board-1", "board-1", "board-2"}
err := store.AddUpdateCategoryBoard(userID, categoryID, boardIDs)
// after
seen := map[string]bool{}
unique := boardIDs[:0]
for _, id := range boardIDs {
	if !seen[id] {
		seen[id] = true
		unique = append(unique, id)
	}
}
err := store.AddUpdateCategoryBoard(userID, categoryID, unique)
Defensive patterns

Strategy: validation

Validate before calling

func validateCategoryUpdate(categoryID string, boardIDs []string) error {
	if len(boardIDs) == 0 { return errors.New("boardIDs empty") }
	seen := map[string]struct{}{}
	for _, id := range boardIDs {
		if _, dup := seen[id]; dup { return fmt.Errorf("duplicate boardID %s", id) }
		seen[id] = struct{}{}
	}
	return nil
}

Type guard

func isCategoryUpsertError(err error) bool {
	return err != nil && strings.Contains(err.Error(), "addUpdateCategoryBoard")
}

Try / catch

err := store.AddUpdateCategoryBoard(userID, categoryID, dedupedBoardIDs)
if err != nil {
	var pqErr *pq.Error
	if errors.As(errors.Unwrap(err), &pqErr) && pqErr.Code.Name() == "unique_violation" {
		// dedupe/re-fetch state and retry once
	}
	return err
}

Prevention

When it happens

Trigger: Calling AddUpdateCategoryBoard with a batch of boardIDs when the upsert fails: invalid categoryID (foreign-key violation if category was deleted), duplicate entries in boardIDs conflicting on (user_id, board_id) within the same statement, oversized batch, schema mismatch on category_boards, or a DB error (deadlock, connection loss) during Exec.

Common situations: Client sends a category update after the category was deleted in another session; batch contains the same board twice (ON CONFLICT DO UPDATE ... cannot affect row a second time in some DBs); migrations left category_boards schema stale; database under heavy write load causing deadlocks.

Related errors


AI-assisted analysis of mattermost-community/focalboard@a84bbb65e3 (2026-08-30). Data as JSON: /api/errors/f8ce2104ec8054cf. Report an issue: GitHub.