From a0baf57767e4ab5f96ce4a34179fbc03cbb421ae Mon Sep 17 00:00:00 2001 From: MAEDA Go Date: Wed, 11 Nov 2026 10:33:01 +0900 Subject: [PATCH] Omit the members subquery from allowed_to_condition for users without memberships Project.allowed_to_condition excludes the projects where the user or its builtin group is a member from the builtin role statement with "projects.id NOT IN (SELECT project_id FROM members ...)". When the user and the builtin group have no memberships, as for the anonymous user, the subquery is always empty and can be omitted. This also works around a MySQL 8.4+ bug that returns wrong results when the empty NOT IN subquery is executed as a hash antijoin inside a dependent subquery, such as the ORDER BY subselect for sorting issues by spent time. Depending on InnoDB statistics, the issue list was sorted incorrectly and IssuesControllerTest#test_index_sort_by_total_spent_hours failed randomly on MySQL. Co-Authored-By: Claude Opus 5.5 --- app/models/project.rb | 11 ++++++++--- test/unit/project_test.rb | 6 ++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 4f7047e1a..5809ab95c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -214,12 +214,17 @@ class Project < ApplicationRecord unless options[:member] role = user.builtin_role if role.allowed_to?(permission, oauth_scope) - s = "#{Project.table_name}.is_public = #{connection.quoted_true}" - if user.id + public_project_condition = "#{Project.table_name}.is_public = #{connection.quoted_true}" + # The subquery is not needed when project_ids_by_role is empty: the + # user and the builtin group have no memberships, so it would return + # nothing + if user.id.nil? || user.project_ids_by_role.empty? + s = public_project_condition + else group = role.anonymous? ? Group.anonymous : Group.non_member principal_ids = [user.id, group.id].compact s = - "(#{s} AND #{Project.table_name}.id NOT IN " \ + "(#{public_project_condition} AND #{Project.table_name}.id NOT IN " \ "(SELECT project_id FROM #{Member.table_name} " \ "WHERE user_id IN (#{principal_ids.join(',')})))" end diff --git a/test/unit/project_test.rb b/test/unit/project_test.rb index d098486cc..76a4dd659 100644 --- a/test/unit/project_test.rb +++ b/test/unit/project_test.rb @@ -1091,6 +1091,12 @@ class ProjectTest < ActiveSupport::TestCase end end + def test_allowed_to_condition_should_not_query_members_for_user_without_memberships + condition = Project.allowed_to_condition(User.anonymous, :view_issues) + + assert_not_includes condition, Member.table_name + end + def test_safe_attributes_should_include_only_custom_fields_visible_to_user cf1 = ProjectCustomField.create!(:name => 'Visible field', :field_format => 'string', -- 2.55.0