Skip to content

Commit 1b0c914

Browse files
authored
Merge pull request rails#42517 from nvasilevski/nilable-default-scope-with-all-queries
Fix update & destroy queries when default_scope is nillable with all_…
2 parents 5d9ee65 + 43d83b4 commit 1b0c914

File tree

3 files changed

+61
-6
lines changed

3 files changed

+61
-6
lines changed

activerecord/lib/active_record/persistence.rb

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -431,9 +431,8 @@ def _insert_record(values) # :nodoc:
431431
def _update_record(values, constraints) # :nodoc:
432432
constraints = constraints.map { |name, value| predicate_builder[name, value] }
433433

434-
if default_scopes?(all_queries: true)
435-
constraints << default_scoped(all_queries: true).where_clause.ast
436-
end
434+
default_constraint = build_default_constraint
435+
constraints << default_constraint if default_constraint
437436

438437
if current_scope = self.global_current_scope
439438
constraints << current_scope.where_clause.ast
@@ -449,9 +448,8 @@ def _update_record(values, constraints) # :nodoc:
449448
def _delete_record(constraints) # :nodoc:
450449
constraints = constraints.map { |name, value| predicate_builder[name, value] }
451450

452-
if default_scopes?(all_queries: true)
453-
constraints << default_scoped(all_queries: true).where_clause.ast
454-
end
451+
default_constraint = build_default_constraint
452+
constraints << default_constraint if default_constraint
455453

456454
if current_scope = self.global_current_scope
457455
constraints << current_scope.where_clause.ast
@@ -479,6 +477,16 @@ def instantiate_instance_of(klass, attributes, column_types = {}, &block)
479477
def discriminate_class_for_record(record)
480478
self
481479
end
480+
481+
# Called by +_update_record+ and +_delete_record+
482+
# to build `where` clause from default scopes.
483+
# Skips empty scopes.
484+
def build_default_constraint
485+
return unless default_scopes?(all_queries: true)
486+
487+
default_where_clause = default_scoped(all_queries: true).where_clause
488+
default_where_clause.ast unless default_where_clause.empty?
489+
end
482490
end
483491

484492
# Returns true if this object hasn't been saved yet -- that is, a record

activerecord/test/cases/scoping/default_scoping_test.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ def test_default_scope_with_all_queries_runs_on_create
9494
assert_match(/mentor_id/, create_sql)
9595
end
9696

97+
def test_nilable_default_scope_with_all_queries_runs_on_create
98+
create_sql = capture_sql { DeveloperWithDefaultNilableMentorScopeAllQueries.create!(name: "Nikita") }.first
99+
100+
assert_no_match(/AND$/, create_sql)
101+
end
102+
97103
def test_default_scope_runs_on_select
98104
Mentor.create!
99105
DeveloperwithDefaultMentorScopeNot.create!(name: "Eileen")
@@ -110,6 +116,13 @@ def test_default_scope_with_all_queries_runs_on_select
110116
assert_match(/mentor_id/, select_sql)
111117
end
112118

119+
def test_nilable_default_scope_with_all_queries_runs_on_select
120+
DeveloperWithDefaultNilableMentorScopeAllQueries.create!(name: "Nikita")
121+
select_sql = capture_sql { DeveloperWithDefaultNilableMentorScopeAllQueries.find_by(name: "Nikita") }.first
122+
123+
assert_no_match(/AND$/, select_sql)
124+
end
125+
113126
def test_default_scope_doesnt_run_on_update
114127
Mentor.create!
115128
dev = DeveloperwithDefaultMentorScopeNot.create!(name: "Eileen")
@@ -126,6 +139,13 @@ def test_default_scope_with_all_queries_runs_on_update
126139
assert_match(/mentor_id/, update_sql)
127140
end
128141

142+
def test_nilable_default_scope_with_all_queries_runs_on_update
143+
dev = DeveloperWithDefaultNilableMentorScopeAllQueries.create!(name: "Nikita")
144+
update_sql = capture_sql { dev.update!(name: "Not Nikita") }.first
145+
146+
assert_no_match(/AND$/, update_sql)
147+
end
148+
129149
def test_default_scope_doesnt_run_on_update_columns
130150
Mentor.create!
131151
dev = DeveloperwithDefaultMentorScopeNot.create!(name: "Eileen")
@@ -142,6 +162,13 @@ def test_default_scope_with_all_queries_runs_on_update_columns
142162
assert_match(/mentor_id/, update_sql)
143163
end
144164

165+
def test_nilable_default_scope_with_all_queries_runs_on_update_columns
166+
dev = DeveloperWithDefaultNilableMentorScopeAllQueries.create!(name: "Nikita")
167+
update_sql = capture_sql { dev.update_columns(name: "Not Nikita") }.first
168+
169+
assert_no_match(/AND$/, update_sql)
170+
end
171+
145172
def test_default_scope_doesnt_run_on_destroy
146173
Mentor.create!
147174
dev = DeveloperwithDefaultMentorScopeNot.create!(name: "Eileen")
@@ -158,6 +185,13 @@ def test_default_scope_with_all_queries_runs_on_destroy
158185
assert_match(/mentor_id/, destroy_sql)
159186
end
160187

188+
def test_nilable_default_scope_with_all_queries_runs_on_destroy
189+
dev = DeveloperWithDefaultNilableMentorScopeAllQueries.create!(name: "Nikita")
190+
destroy_sql = capture_sql { dev.destroy }.first
191+
192+
assert_no_match(/AND$/, destroy_sql)
193+
end
194+
161195
def test_default_scope_doesnt_run_on_reload
162196
Mentor.create!
163197
dev = DeveloperwithDefaultMentorScopeNot.create!(name: "Eileen")
@@ -174,6 +208,13 @@ def test_default_scope_with_all_queries_runs_on_reload
174208
assert_match(/mentor_id/, reload_sql)
175209
end
176210

211+
def test_nilable_default_scope_with_all_queries_runs_on_destroy
212+
dev = DeveloperWithDefaultNilableMentorScopeAllQueries.create!(name: "Nikita")
213+
reload_sql = capture_sql { dev.reload }.first
214+
215+
assert_no_match(/AND$/, reload_sql)
216+
end
217+
177218
def test_default_scope_with_all_queries_doesnt_run_on_destroy_when_unscoped
178219
dev = DeveloperWithDefaultMentorScopeAllQueries.create!(name: "Eileen", mentor_id: 2)
179220
reload_sql = capture_sql { dev.reload({ unscoped: true }) }.first

activerecord/test/models/developer.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,12 @@ class DeveloperWithDefaultMentorScopeAllQueries < ActiveRecord::Base
162162
default_scope -> { where(mentor_id: 1) }, all_queries: true
163163
end
164164

165+
class DeveloperWithDefaultNilableMentorScopeAllQueries < ActiveRecord::Base
166+
self.table_name = "developers"
167+
firm_id = nil # Could be something like Current.mentor_id
168+
default_scope -> { where(firm_id: firm_id) if firm_id }, all_queries: true
169+
end
170+
165171
class DeveloperWithIncludes < ActiveRecord::Base
166172
self.table_name = "developers"
167173
has_many :audit_logs, foreign_key: :developer_id

0 commit comments

Comments
 (0)