activerecord-multi-tenant 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 457cfac4996b93bb7b0b25cd99c2687f2742a8bc580888008baa4f903255cd74
4
- data.tar.gz: 8c369e04a906752deda52d9b7b78a2377551e4b85f74e3b6d4b0742d30147783
3
+ metadata.gz: e198970d8d5a4db04c8e203e28021668c765fdde78f9bb9a7cd0e6c577bfbe0e
4
+ data.tar.gz: 6abb30aac520063dbef7ddc5689714ef1dd9eb691aa382498b30c8bbcf03844b
5
5
  SHA512:
6
- metadata.gz: 7715dadcbd9cd13d86135518d3fc62283d9ab85e802016ab1be15fe39d815ec344c1d3e4699f96870b6e50b6d3b66b4a38f780d07ad6d3d4b8e21668deacca18
7
- data.tar.gz: d1d364ebde87012cc5b91a6cf812a1a8cc0f89bb5ee8859b3de01ab0392dc5a62b74a3f93ac721d3e583343b703fc872d73c1f301cff9716e686492f06922d36
6
+ metadata.gz: 5010dbbd2a8036ad5a3adae79a49567a279a318931ffe832663dd2430f455d1895e26cca6d96456484b894cc241cca21fbab31aa301f9ab4ef0538a4266b4005
7
+ data.tar.gz: e94b7ee0f619b0763879efefbae3a69792e00e8b9f2a2661a82666d4c129decea132f20b44999177bdc0162dec0e9b3cac4f402d70a76fb14518e283f4441d6c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.1.0 2022-10-20
4
+ * Fix query building for models with mismatched partition_keys [#150](https://github.com/citusdata/activerecord-multi-tenant/pull/150)
5
+ * Identify tenant even if class name is nonstandard [#152](https://github.com/citusdata/activerecord-multi-tenant/pull/152)
6
+ * Add current_tenant_id to WHERE clauses when calling methods on activerecord instance or its associations [#154](https://github.com/citusdata/activerecord-multi-tenant/pull/154)
7
+ * Make create_distributed_table, create_reference_table reversible & add ruby wrapper for rebalance_table_shards [#155](https://github.com/citusdata/activerecord-multi-tenant/pull/155)
8
+ * Support create_distributed_table, create_reference_table in schema.rb [#156](https://github.com/citusdata/activerecord-multi-tenant/pull/156)
9
+ * Add client and server sidekiq middleware to sidekiq middleware chain [#158](https://github.com/citusdata/activerecord-multi-tenant/pull/158)
10
+
3
11
  ## 2.0.0 2022-05-19
4
12
 
5
13
  * Replace RequestStore with CurrentAttributes [#139](https://github.com/citusdata/activerecord-multi-tenant/pull/139)
@@ -2,12 +2,40 @@ module MultiTenant
2
2
  module MigrationExtensions
3
3
  def create_distributed_table(table_name, partition_key)
4
4
  return unless citus_version.present?
5
- execute "SELECT create_distributed_table($$#{table_name}$$, $$#{partition_key}$$)"
5
+
6
+ reversible do |dir|
7
+ dir.up do
8
+ execute "SELECT create_distributed_table($$#{table_name}$$, $$#{partition_key}$$)"
9
+ end
10
+ dir.down do
11
+ undistribute_table(table_name)
12
+ end
13
+ end
6
14
  end
7
15
 
8
16
  def create_reference_table(table_name)
9
17
  return unless citus_version.present?
10
- execute "SELECT create_reference_table($$#{table_name}$$)"
18
+
19
+ reversible do |dir|
20
+ dir.up do
21
+ execute "SELECT create_reference_table($$#{table_name}$$)"
22
+ end
23
+ dir.down do
24
+ undistribute_table(table_name)
25
+ end
26
+ end
27
+ end
28
+
29
+ def undistribute_table(table_name)
30
+ return unless citus_version.present?
31
+
32
+ execute "SELECT undistribute_table($$#{table_name}$$))"
33
+ end
34
+
35
+ def rebalance_table_shards
36
+ return unless citus_version.present?
37
+
38
+ execute 'SELECT rebalance_table_shards()'
11
39
  end
12
40
 
13
41
  def execute_on_all_nodes(sql)
@@ -28,21 +56,19 @@ module MultiTenant
28
56
  end
29
57
 
30
58
  def citus_version
31
- execute("SELECT extversion FROM pg_extension WHERE extname = 'citus'").getvalue(0,0).try(:split, '-').try(:first)
59
+ execute("SELECT extversion FROM pg_extension WHERE extname = 'citus'").getvalue(0, 0).try(:split, '-').try(:first)
32
60
  rescue ArgumentError => e
33
- raise unless e.message == "invalid tuple number 0"
61
+ raise unless e.message == 'invalid tuple number 0'
34
62
  end
35
63
  end
36
64
  end
37
65
 
38
- if defined?(ActiveRecord::Migration)
39
- ActiveRecord::Migration.send(:include, MultiTenant::MigrationExtensions)
40
- end
66
+ ActiveRecord::Migration.include MultiTenant::MigrationExtensions if defined?(ActiveRecord::Migration)
41
67
 
42
68
  module ActiveRecord
43
69
  module ConnectionAdapters # :nodoc:
44
70
  module SchemaStatements
45
- alias :orig_create_table :create_table
71
+ alias orig_create_table create_table
46
72
  def create_table(table_name, options = {}, &block)
47
73
  ret = orig_create_table(table_name, **options.except(:partition_key), &block)
48
74
  if options[:partition_key] && options[:partition_key].to_s != 'id'
@@ -54,3 +80,38 @@ module ActiveRecord
54
80
  end
55
81
  end
56
82
  end
83
+
84
+ module ActiveRecord
85
+ class SchemaDumper
86
+ private
87
+
88
+ alias initialize_without_citus initialize
89
+ def initialize(connection, options = {})
90
+ initialize_without_citus(connection, options)
91
+
92
+ @distribution_columns =
93
+ if ActiveRecord::Migration.citus_version.present?
94
+ @connection.execute('SELECT logicalrelid::regclass AS table_name, column_to_column_name(logicalrelid, partkey) AS dist_col_name FROM pg_dist_partition').to_h do |v|
95
+ [v['table_name'], v['dist_col_name']]
96
+ end
97
+ else
98
+ {}
99
+ end
100
+ end
101
+
102
+ # Support for create_distributed_table & create_reference_table
103
+ alias table_without_citus table
104
+ def table(table, stream)
105
+ table_without_citus(table, stream)
106
+ table_name = remove_prefix_and_suffix(table)
107
+ distribution_column = @distribution_columns[table_name]
108
+ if distribution_column
109
+ stream.puts " create_distributed_table(#{table_name.inspect}, #{distribution_column.inspect})"
110
+ stream.puts
111
+ elsif @distribution_columns.key?(table_name)
112
+ stream.puts " create_reference_table(#{table_name.inspect})"
113
+ stream.puts
114
+ end
115
+ end
116
+ end
117
+ end
@@ -3,7 +3,7 @@ module MultiTenant
3
3
  DEFAULT_ID_FIELD = 'id'.freeze
4
4
 
5
5
  def multi_tenant(tenant_name, options = {})
6
- if to_s.underscore.to_sym == tenant_name
6
+ if to_s.underscore.to_sym == tenant_name || (!table_name.nil? && table_name.singularize.to_sym == tenant_name)
7
7
  unless MultiTenant.with_write_only_mode_enabled?
8
8
  # This is the tenant model itself. Workaround for https://github.com/citusdata/citus/issues/687
9
9
  before_create -> do
@@ -129,6 +129,20 @@ end
129
129
 
130
130
  ActiveSupport.on_load(:active_record) do |base|
131
131
  base.extend MultiTenant::ModelExtensionsClassMethods
132
+
133
+ # Ensure we have current_tenant_id in where clause when a cached ActiveRecord instance is being reloaded, or update_columns without callbacks is called
134
+ MultiTenant.wrap_methods(ActiveRecord::Base, 'self', :delete, :reload, :update_columns)
135
+
136
+ # Any queuries fired for fetching a singular association have the correct current_tenant_id in WHERE clause
137
+ # reload is called anytime any record's association is accessed
138
+ MultiTenant.wrap_methods(ActiveRecord::Associations::Association, 'owner', :reload)
139
+
140
+ # For collection associations, we need to wrap multiple methods in returned proxy so that any queries have the correct current_tenant_id in WHERE clause
141
+ ActiveRecord::Associations::CollectionProxy.alias_method :equals_mt, :== # Hack to prevent syntax error due to invalid method name
142
+ ActiveRecord::Associations::CollectionProxy.alias_method :append_mt, :<< # Hack to prevent syntax error due to invalid method name
143
+ MultiTenant.wrap_methods(ActiveRecord::Associations::CollectionProxy, '@association.owner', :find, :last, :take, :build, :create, :create!, :replace, :delete_all, :destroy_all, :delete, :destroy, :calculate, :pluck, :size, :empty?, :include?, :equals_mt, :records, :append_mt, :find_nth_with_limit, :find_nth_from_last, :null_scope?, :find_from_target?, :exec_queries)
144
+ ActiveRecord::Associations::CollectionProxy.alias_method :==, :equals_mt
145
+ ActiveRecord::Associations::CollectionProxy.alias_method :<<, :append_mt
132
146
  end
133
147
 
134
148
  class ActiveRecord::Associations::Association
@@ -109,6 +109,23 @@ module MultiTenant
109
109
  end
110
110
  end
111
111
 
112
+ # Wrap calls to any of `method_names` on an instance Class `klass` with MultiTenant.with when `'owner'` (evaluated in context of the klass instance) is a ActiveRecord model instance that is multi-tenant
113
+ def self.wrap_methods(klass, owner, *method_names)
114
+ method_names.each do |method_name|
115
+ original_method_name = :"_mt_original_#{method_name}"
116
+ klass.class_eval <<-CODE, __FILE__, __LINE__ + 1
117
+ alias_method :#{original_method_name}, :#{method_name}
118
+ def #{method_name}(*args, &block)
119
+ if MultiTenant.multi_tenant_model_for_table(#{owner}.class.table_name).present? && #{owner}.persisted? && MultiTenant.current_tenant_id.nil?
120
+ MultiTenant.with(#{owner}.public_send(#{owner}.class.partition_key)) { #{original_method_name}(*args) }
121
+ else
122
+ #{original_method_name}(*args)
123
+ end
124
+ end
125
+ CODE
126
+ end
127
+ end
128
+
112
129
  # Preserve backward compatibility for people using .with_id
113
130
  singleton_class.send(:alias_method, :with_id, :with)
114
131
 
@@ -305,7 +305,7 @@ module ActiveRecord
305
305
  model_right = MultiTenant.multi_tenant_model_for_table(relation_left.table_name)
306
306
  model_left = MultiTenant.multi_tenant_model_for_table(relation_right.table_name)
307
307
  if model_right && model_left
308
- join_enforcement_clause = MultiTenant::TenantJoinEnforcementClause.new(relation_left[model_left.partition_key], relation_right)
308
+ join_enforcement_clause = MultiTenant::TenantJoinEnforcementClause.new(relation_right[model_right.partition_key], relation_left)
309
309
  node_join.right.expr = node_join.right.expr.and(join_enforcement_clause)
310
310
  end
311
311
  end
@@ -33,6 +33,22 @@ module Sidekiq::Middleware::MultiTenant
33
33
  end
34
34
  end
35
35
 
36
+ Sidekiq.configure_server do |config|
37
+ config.server_middleware do |chain|
38
+ chain.add Sidekiq::Middleware::MultiTenant::Server
39
+ end
40
+ config.client_middleware do |chain|
41
+ chain.add Sidekiq::Middleware::MultiTenant::Client
42
+ end
43
+ end
44
+
45
+ Sidekiq.configure_client do |config|
46
+ config.client_middleware do |chain|
47
+ chain.add Sidekiq::Middleware::MultiTenant::Client
48
+ end
49
+ end
50
+
51
+
36
52
  module Sidekiq
37
53
  class Client
38
54
  def push_bulk_with_tenants(items)
@@ -1,3 +1,3 @@
1
1
  module MultiTenant
2
- VERSION = '2.0.0'
2
+ VERSION = '2.1.1'
3
3
  end
@@ -10,4 +10,3 @@ require_relative 'activerecord-multi-tenant/query_rewriter'
10
10
  require_relative 'activerecord-multi-tenant/query_monitor'
11
11
  require_relative 'activerecord-multi-tenant/version'
12
12
  require_relative 'activerecord-multi-tenant/with_lock'
13
- require_relative 'activerecord-multi-tenant/persistence_extension'
@@ -0,0 +1,21 @@
1
+ require 'spec_helper'
2
+
3
+ describe MultiTenant, 'Association methods' do
4
+ let(:account1) { Account.create! name: 'test1' }
5
+ let(:account2) { Account.create! name: 'test2' }
6
+ let(:project1) { Project.create! name: 'something1', account: account1 }
7
+ let(:project2) { Project.create! name: 'something2', account: account2, id: project1.id }
8
+ let(:task1) { Task.create! name: 'task1', project: project1, account: account1 }
9
+ let(:task2) { Task.create! name: 'task2', project: project2, account: account2, id: task1.id }
10
+
11
+ context 'include the tenant_id in queries and' do
12
+ it 'creates a task with correct account_id' do
13
+ expect(project2.tasks.create(name: 'task3').account_id).to eq(account2.id)
14
+ end
15
+ it 'return correct account_id' do
16
+ expect(task1.project.account_id).to_not eq(task2.project.account_id) # belongs_to
17
+ expect(project2.tasks.count).to eq(1)
18
+ expect(project2.tasks.first.account_id).to eq(account2.id) # has_many
19
+ end
20
+ end
21
+ end
@@ -70,6 +70,24 @@ describe MultiTenant do
70
70
  it { expect(@partition_key_not_model_task.non_model_id).to be 77 }
71
71
  end
72
72
 
73
+
74
+ describe 'Tenant model with a nonstandard class name' do
75
+ let(:account_klass) do
76
+ Class.new(ActiveRecord::Base) do
77
+ self.table_name = 'account'
78
+ def self.name
79
+ 'UserAccount'
80
+ end
81
+
82
+ multi_tenant(:account)
83
+ end
84
+ end
85
+ it "does not register the tenant model" do
86
+ expect(MultiTenant).not_to receive(:register_multi_tenant_model)
87
+ account_klass
88
+ end
89
+ end
90
+
73
91
  describe 'Changes table_name after multi_tenant called' do
74
92
  before do
75
93
  account_klass.has_many(:posts, anonymous_class: post_klass)
@@ -186,11 +204,7 @@ describe MultiTenant do
186
204
  end
187
205
 
188
206
  it 'handles belongs_to with optional: true' do
189
- MultiTenant.with(account) do
190
- sub_task
191
- end
192
-
193
- record = sub_task.optional_sub_tasks.create!
207
+ record = OptionalSubTask.create(sub_task_id: sub_task.id)
194
208
  expect(record.reload.sub_task).to eq(sub_task)
195
209
  expect(record.account_id).to eq(nil)
196
210
  end
@@ -400,10 +414,10 @@ describe MultiTenant do
400
414
 
401
415
  it "applies the team_id conditions in the where clause" do
402
416
  option1 = <<-sql.strip
403
- SELECT "sub_tasks".* FROM "sub_tasks" INNER JOIN "tasks" ON "sub_tasks"."task_id" = "tasks"."id" AND "sub_tasks"."account_id" = "tasks"."account_id" WHERE "tasks"."project_id" = 1 AND "sub_tasks"."account_id" = 1 AND "tasks"."account_id" = 1
417
+ SELECT "sub_tasks".* FROM "sub_tasks" INNER JOIN "tasks" ON "sub_tasks"."task_id" = "tasks"."id" AND "tasks"."account_id" = "sub_tasks"."account_id" WHERE "tasks"."project_id" = 1 AND "sub_tasks"."account_id" = 1 AND "tasks"."account_id" = 1
404
418
  sql
405
419
  option2 = <<-sql.strip
406
- SELECT "sub_tasks".* FROM "sub_tasks" INNER JOIN "tasks" ON "sub_tasks"."task_id" = "tasks"."id" AND "sub_tasks"."account_id" = "tasks"."account_id" WHERE "sub_tasks"."account_id" = 1 AND "tasks"."project_id" = 1 AND "tasks"."account_id" = 1
420
+ SELECT "sub_tasks".* FROM "sub_tasks" INNER JOIN "tasks" ON "sub_tasks"."task_id" = "tasks"."id" AND "tasks"."account_id" = "sub_tasks"."account_id" WHERE "sub_tasks"."account_id" = 1 AND "tasks"."project_id" = 1 AND "tasks"."account_id" = 1
407
421
  sql
408
422
 
409
423
  account1 = Account.create! name: 'Account 1'
@@ -418,7 +432,7 @@ describe MultiTenant do
418
432
 
419
433
  MultiTenant.without do
420
434
  expected_sql = <<-sql
421
- SELECT "sub_tasks".* FROM "sub_tasks" INNER JOIN "tasks" ON "sub_tasks"."task_id" = "tasks"."id" AND "sub_tasks"."account_id" = "tasks"."account_id" WHERE "tasks"."project_id" = 1
435
+ SELECT "sub_tasks".* FROM "sub_tasks" INNER JOIN "tasks" ON "sub_tasks"."task_id" = "tasks"."id" AND "tasks"."account_id" = "sub_tasks"."account_id" WHERE "tasks"."project_id" = 1
422
436
  sql
423
437
 
424
438
  project = Project.first
@@ -456,7 +470,7 @@ describe MultiTenant do
456
470
  expect(project.categories).to include(category1)
457
471
 
458
472
  expected_sql = <<-sql
459
- SELECT "projects".* FROM "projects" INNER JOIN "project_categories" ON "project_categories"."project_id" = "projects"."id" AND "project_categories"."account_id" = "projects"."account_id" INNER JOIN "categories" ON "categories"."id" = "project_categories"."category_id" WHERE "projects"."account_id" = 1
473
+ SELECT "projects".* FROM "projects" INNER JOIN "project_categories" ON "project_categories"."project_id" = "projects"."id" AND "projects"."account_id" = "project_categories"."account_id" INNER JOIN "categories" ON "categories"."id" = "project_categories"."category_id" WHERE "projects"."account_id" = 1
460
474
  sql
461
475
 
462
476
  expect(Project.where(account_id: 1).joins(:categories).to_sql).to eq(expected_sql.strip)
@@ -490,7 +504,7 @@ describe MultiTenant do
490
504
 
491
505
  MultiTenant.without do
492
506
  expected_sql = <<-sql
493
- SELECT "projects"."id" AS t0_r0, "projects"."account_id" AS t0_r1, "projects"."name" AS t0_r2, "categories"."id" AS t1_r0, "categories"."name" AS t1_r1 FROM "projects" LEFT OUTER JOIN "project_categories" ON "project_categories"."project_id" = "projects"."id" AND "project_categories"."account_id" = "projects"."account_id" LEFT OUTER JOIN "categories" ON "categories"."id" = "project_categories"."category_id" WHERE "projects"."account_id" = 1
507
+ SELECT "projects"."id" AS t0_r0, "projects"."account_id" AS t0_r1, "projects"."name" AS t0_r2, "categories"."id" AS t1_r0, "categories"."name" AS t1_r1 FROM "projects" LEFT OUTER JOIN "project_categories" ON "project_categories"."project_id" = "projects"."id" AND "projects"."account_id" = "project_categories"."account_id" LEFT OUTER JOIN "categories" ON "categories"."id" = "project_categories"."category_id" WHERE "projects"."account_id" = 1
494
508
  sql
495
509
 
496
510
  expect(Project.where(account_id: 1).eager_load(:categories).to_sql).to eq(expected_sql.strip)
@@ -522,7 +536,7 @@ describe MultiTenant do
522
536
 
523
537
  MultiTenant.without do
524
538
  expected_sql = <<-sql
525
- SELECT "tasks".* FROM "tasks" INNER JOIN "projects" ON "projects"."id" = "tasks"."project_id" AND "projects"."account_id" = "tasks"."account_id" LEFT JOIN project_categories pc ON project.category_id = pc.id WHERE "tasks"."account_id" = 1
539
+ SELECT "tasks".* FROM "tasks" INNER JOIN "projects" ON "projects"."id" = "tasks"."project_id" AND "tasks"."account_id" = "projects"."account_id" LEFT JOIN project_categories pc ON project.category_id = pc.id WHERE "tasks"."account_id" = 1
526
540
  sql
527
541
 
528
542
  expect(Task.where(account_id: 1).joins(:project).joins('LEFT JOIN project_categories pc ON project.category_id = pc.id').to_sql).to eq(expected_sql.strip)
@@ -54,6 +54,25 @@ describe MultiTenant, 'Record modifications' do
54
54
  end
55
55
  end
56
56
 
57
+ it 'should not update other objects with same id when calling object.update_columns' do
58
+ # When two records with same id but different account_id are updated, it should only update the current one
59
+ expect(project.account).to eq(account)
60
+ expect(project2.account).to eq(account2)
61
+ expect(project.id).to eq(project2.id)
62
+
63
+ MultiTenant.without do
64
+ project2.update_columns(name: 'newthing2')
65
+ expect(project.reload.name).to eq('something')
66
+ expect(project2.reload.name).to eq('newthing2')
67
+ end
68
+ end
69
+
70
+ it 'should return the same object when calling object.reload' do
71
+ # When two records with same id but different account_id are updated, it should not return the other object
72
+ expect(project.reload.account_id).to eq(account.id)
73
+ expect(project2.reload.account_id).to eq(account2.id)
74
+ end
75
+
57
76
  it 'test delete for reference tables' do
58
77
  category1 = Category.create! name: 'Category 1'
59
78
  expect(Category.count).to eq(1)
data/spec/schema.rb CHANGED
@@ -179,7 +179,6 @@ end
179
179
  with_belongs_to_required_by_default do
180
180
  class OptionalSubTask < ActiveRecord::Base
181
181
  multi_tenant :account, optional: true
182
- belongs_to :account, optional: true
183
182
  belongs_to :sub_task
184
183
  end
185
184
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-multi-tenant
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Citus Data
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-05-19 00:00:00.000000000 Z
11
+ date: 2022-10-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -172,12 +172,12 @@ files:
172
172
  - lib/activerecord-multi-tenant/migrations.rb
173
173
  - lib/activerecord-multi-tenant/model_extensions.rb
174
174
  - lib/activerecord-multi-tenant/multi_tenant.rb
175
- - lib/activerecord-multi-tenant/persistence_extension.rb
176
175
  - lib/activerecord-multi-tenant/query_monitor.rb
177
176
  - lib/activerecord-multi-tenant/query_rewriter.rb
178
177
  - lib/activerecord-multi-tenant/sidekiq.rb
179
178
  - lib/activerecord-multi-tenant/version.rb
180
179
  - lib/activerecord-multi-tenant/with_lock.rb
180
+ - spec/activerecord-multi-tenant/associations_spec.rb
181
181
  - spec/activerecord-multi-tenant/controller_extensions_spec.rb
182
182
  - spec/activerecord-multi-tenant/fast_truncate_spec.rb
183
183
  - spec/activerecord-multi-tenant/model_extensions_spec.rb
@@ -1,13 +0,0 @@
1
- module ActiveRecord
2
- module Persistence
3
- alias :delete_orig :delete
4
-
5
- def delete
6
- if MultiTenant.multi_tenant_model_for_table(self.class.table_name).present? && persisted? && MultiTenant.current_tenant_id.nil?
7
- MultiTenant.with(self.public_send(self.class.partition_key)) { delete_orig }
8
- else
9
- delete_orig
10
- end
11
- end
12
- end
13
- end