Skip to content

Commit e3d2c88

Browse files
authored
Fix the tenant scoping for update_all (#223)
1 parent 20d09a8 commit e3d2c88

File tree

3 files changed

+136
-8
lines changed

3 files changed

+136
-8
lines changed

lib/activerecord-multi-tenant/delete_operations.rb lib/activerecord-multi-tenant/relation_extension.rb

+33-7
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,55 @@ def delete_all
77
# Call the original delete_all method if the current tenant is identified by an ID
88
return super if MultiTenant.current_tenant_is_id? || MultiTenant.current_tenant.nil?
99

10+
stmt = Arel::DeleteManager.new.from(table)
11+
stmt.wheres = [generate_in_condition_subquery]
12+
13+
# Execute the delete statement using the connection and return the result
14+
klass.connection.delete(stmt, "#{klass} Delete All").tap { reset }
15+
end
16+
17+
# Overrides the update_all method to include tenant scoping
18+
def update_all(updates)
19+
# Call the original update_all method if the current tenant is identified by an ID
20+
return super if MultiTenant.current_tenant_is_id? || MultiTenant.current_tenant.nil?
21+
22+
stmt = Arel::UpdateManager.new
23+
stmt.table(table)
24+
stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates))
25+
stmt.wheres = [generate_in_condition_subquery]
26+
27+
klass.connection.update(stmt, "#{klass} Update All").tap { reset }
28+
end
29+
30+
private
31+
32+
# The generate_in_condition_subquery method generates a subquery that selects
33+
# records associated with the current tenant.
34+
def generate_in_condition_subquery
35+
# Get the tenant key and tenant ID based on the current tenant
1036
tenant_key = MultiTenant.partition_key(MultiTenant.current_tenant_class)
1137
tenant_id = MultiTenant.current_tenant_id
38+
39+
# Build an Arel query
1240
arel = eager_loading? ? apply_join_dependency.arel : build_arel
1341
arel.source.left = table
1442

43+
# If the tenant ID is present and the tenant key is a column in the model,
44+
# add a condition to only include records where the tenant key equals the tenant ID
1545
if tenant_id && klass.column_names.include?(tenant_key)
16-
# Check if the tenant key is present in the model's column names
1746
tenant_condition = table[tenant_key].eq(tenant_id)
18-
# Add the tenant condition to the arel query if it is not already present
1947
unless arel.constraints.any? { |node| node.to_sql.include?(tenant_condition.to_sql) }
2048
arel = arel.where(tenant_condition)
2149
end
2250
end
2351

52+
# Clone the query, clear its projections, and set its projection to the primary key of the table
2453
subquery = arel.clone
2554
subquery.projections.clear
2655
subquery = subquery.project(table[primary_key])
27-
in_condition = Arel::Nodes::In.new(table[primary_key], subquery.ast)
28-
stmt = Arel::DeleteManager.new.from(table)
29-
stmt.wheres = [in_condition]
3056

31-
# Execute the delete statement using the connection and return the result
32-
klass.connection.delete(stmt, "#{klass} Delete All").tap { reset }
57+
# Create an IN condition node with the primary key of the table and the subquery
58+
Arel::Nodes::In.new(table[primary_key], subquery.ast)
3359
end
3460
end
3561
end

lib/activerecord_multi_tenant.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@
1111
require_relative 'activerecord-multi-tenant/table_node'
1212
require_relative 'activerecord-multi-tenant/version'
1313
require_relative 'activerecord-multi-tenant/habtm'
14-
require_relative 'activerecord-multi-tenant/delete_operations'
14+
require_relative 'activerecord-multi-tenant/relation_extension'

spec/activerecord-multi-tenant/query_rewriter_spec.rb

+102
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33
require 'spec_helper'
44

55
describe 'Query Rewriter' do
6+
before(:each) do
7+
@queries = []
8+
ActiveSupport::Notifications.subscribe('sql.active_record') do |_name, _started, _finished, _unique_id, payload|
9+
@queries << payload[:sql]
10+
end
11+
end
12+
613
context 'when bulk updating' do
714
let!(:account) { Account.create!(name: 'Test Account') }
815
let!(:project) { Project.create(name: 'Project 1', account: account) }
@@ -35,6 +42,67 @@
3542
project.update(name: 'New Name')
3643
end.to change { project.reload.name }.from('Project 1').to('New Name')
3744
end
45+
46+
it 'update_all the records with expected query' do
47+
expected_query = <<-SQL.strip
48+
UPDATE "projects" SET "name" = 'New Name' WHERE "projects"."id" IN
49+
(SELECT "projects"."id" FROM "projects"
50+
INNER JOIN "managers" ON "managers"."project_id" = "projects"."id"
51+
and "managers"."account_id" = :account_id
52+
WHERE "projects"."account_id" = :account_id
53+
)
54+
AND "projects"."account_id" = :account_id
55+
SQL
56+
57+
expect do
58+
MultiTenant.with(account) do
59+
Project.joins(:manager).update_all(name: 'New Name')
60+
end
61+
end.to change { project.reload.name }.from('Project 1').to('New Name')
62+
63+
@queries.each do |actual_query|
64+
next unless actual_query.include?('UPDATE "projects" SET "name"')
65+
66+
expect(format_sql(actual_query)).to eq(format_sql(expected_query.gsub(':account_id', account.id.to_s)))
67+
end
68+
end
69+
70+
it 'updates a limited number of records with expected query' do
71+
# create 2 more projects
72+
Project.create(name: 'project2', account: account)
73+
Project.create(name: 'project3', account: account)
74+
new_name = 'New Name'
75+
limit = 2
76+
expected_query = <<-SQL
77+
UPDATE
78+
"projects"
79+
SET
80+
"name" = 'New Name'
81+
WHERE
82+
"projects"."id" IN (
83+
SELECT
84+
"projects"."id"
85+
FROM
86+
"projects"
87+
WHERE
88+
"projects"."account_id" = #{account.id} LIMIT #{limit}
89+
)
90+
AND "projects"."account_id" = #{account.id}
91+
SQL
92+
93+
expect do
94+
MultiTenant.with(account) do
95+
Project.limit(limit).update_all(name: new_name)
96+
end
97+
end.to change { Project.where(name: new_name).count }.from(0).to(limit)
98+
99+
@queries.each do |actual_query|
100+
next unless actual_query.include?('UPDATE "projects" SET "name"')
101+
102+
expect(format_sql(actual_query.gsub('$1',
103+
limit.to_s)).strip).to eq(format_sql(expected_query).strip)
104+
end
105+
end
38106
end
39107

40108
context 'when bulk deleting' do
@@ -102,6 +170,40 @@
102170
end.to change { Project.count }.from(3).to(1)
103171
end
104172

173+
it 'deletes a limited number of records with expected query' do
174+
# create 2 more projects
175+
Project.create(name: 'project2', account: account)
176+
Project.create(name: 'project3', account: account)
177+
limit = 2
178+
expected_query = <<-SQL
179+
DELETE FROM
180+
"projects"
181+
WHERE
182+
"projects"."id" IN (
183+
SELECT
184+
"projects"."id"
185+
FROM
186+
"projects"
187+
WHERE
188+
"projects"."account_id" = #{account.id} LIMIT #{limit}
189+
)
190+
AND "projects"."account_id" = #{account.id}
191+
SQL
192+
193+
expect do
194+
MultiTenant.with(account) do
195+
Project.limit(limit).delete_all
196+
end
197+
end.to change { Project.count }.by(-limit)
198+
199+
@queries.each do |actual_query|
200+
next unless actual_query.include?('DELETE FROM "projects"')
201+
202+
expect(format_sql(actual_query.gsub('$1',
203+
limit.to_s)).strip).to eq(format_sql(expected_query).strip)
204+
end
205+
end
206+
105207
it 'destroy the record' do
106208
expect do
107209
MultiTenant.with(account) do

0 commit comments

Comments
 (0)