From d7f53c5f774205af045e0217d2e84f55137d194d Mon Sep 17 00:00:00 2001 From: MAEDA Go Date: Wed, 2 Sep 2026 16:40:10 +0900 Subject: [PATCH] Skip the nested set self-join in Issue.load_visible_total_spent_hours when all issues are leaves. --- app/models/issue.rb | 18 ++++++++++++++---- test/unit/issue_test.rb | 16 ++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/app/models/issue.rb b/app/models/issue.rb index fa2d7707d..8a55fc361 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -1258,14 +1258,24 @@ class Issue < ApplicationRecord # Preloads visible total spent time for a collection of issues def self.load_visible_total_spent_hours(issues, user=User.current) - if issues.any? + return if issues.empty? + + if issues.all?(&:leaf?) + # None of the issues has subtasks, so the expensive nested set + # self-join can be skipped. + # Keep this query identical to the one in load_visible_spent_hours so + # that it is served from the query cache when both are called in the + # same request (e.g. IssuesController#show and #index). + hours_by_issue_id = TimeEntry.visible(user). + where(:issue_id => issues.map(&:id)).group(:issue_id).sum(:hours) + else hours_by_issue_id = TimeEntry.visible(user).joins(:issue). joins("JOIN #{Issue.table_name} parent ON parent.root_id = #{Issue.table_name}.root_id" + " AND parent.lft <= #{Issue.table_name}.lft AND parent.rgt >= #{Issue.table_name}.rgt"). where("parent.id IN (?)", issues.map(&:id)).group("parent.id").sum(:hours) - issues.each do |issue| - issue.instance_variable_set :@total_spent_hours, (hours_by_issue_id[issue.id] || 0.0) - end + end + issues.each do |issue| + issue.instance_variable_set :@total_spent_hours, (hours_by_issue_id[issue.id] || 0.0) end end diff --git a/test/unit/issue_test.rb b/test/unit/issue_test.rb index cc92b8f8e..3d4336f03 100644 --- a/test/unit/issue_test.rb +++ b/test/unit/issue_test.rb @@ -240,6 +240,22 @@ class IssueTest < ActiveSupport::TestCase assert_include 'Parent task is invalid', issue.errors.full_messages end + def test_load_visible_total_spent_hours_should_load_visible_hours_for_leaf_issues + issues = Issue.where(:id => [1, 2]).to_a + assert issues.all?(&:leaf?) + user = User.generate! + role = Role.generate!(:permissions => [:view_issues, :view_time_entries], :time_entries_visibility => 'own') + User.add_to_project(user, Project.find(1), role) + TimeEntry.generate!(:issue => issues[0], :hours => 100.0, :user => User.find(2)) + TimeEntry.generate!(:issue => issues[1], :hours => 100.0, :user => User.find(2)) + TimeEntry.generate!(:issue => issues[1], :hours => 2.5, :user => user) + + Issue.load_visible_total_spent_hours(issues, user) + + assert_equal 0.0, issues[0].total_spent_hours + assert_equal 2.5, issues[1].total_spent_hours + end + def assert_visibility_match(user, issues) assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort end -- 2.55.0