activerecord-multi-tenant 2.0.0 → 2.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 457cfac4996b93bb7b0b25cd99c2687f2742a8bc580888008baa4f903255cd74
4
- data.tar.gz: 8c369e04a906752deda52d9b7b78a2377551e4b85f74e3b6d4b0742d30147783
3
+ metadata.gz: 6da68db2d8d58cc00bc0fd9de50b85544fff943596c4ed7fb9211380419e1b1b
4
+ data.tar.gz: a5126bdd3b733a6e58c0c5d1e63e7bfe27b76a18fd790219ee08641379019336
5
5
  SHA512:
6
- metadata.gz: 7715dadcbd9cd13d86135518d3fc62283d9ab85e802016ab1be15fe39d815ec344c1d3e4699f96870b6e50b6d3b66b4a38f780d07ad6d3d4b8e21668deacca18
7
- data.tar.gz: d1d364ebde87012cc5b91a6cf812a1a8cc0f89bb5ee8859b3de01ab0392dc5a62b74a3f93ac721d3e583343b703fc872d73c1f301cff9716e686492f06922d36
6
+ metadata.gz: 69be1d704a41ee78b28c9f23cb8bb1f606a97146c979ca087eeef1d43fa4a482bacedc21317677862f427567d98abcc4199f029eb04719b2dab363bb29a2160d
7
+ data.tar.gz: a6495adf22bbd72f03e15f8056e57c9dbabfaab9dde1b15fc3365794a1a8c25f624e094e82de2b7ed1648dcad990b410b6f3b97e6369534aa7ea6ee528505360
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.1.2 2022-10-26
4
+ * Fixes issue when wraping methods that require a block [#162](https://github.com/citusdata/activerecord-multi-tenant/pull/162)
5
+
6
+ ## 2.1.1 2022-10-20
7
+ * Fix query building for models with mismatched partition_keys [#150](https://github.com/citusdata/activerecord-multi-tenant/pull/150)
8
+ * Identify tenant even if class name is nonstandard [#152](https://github.com/citusdata/activerecord-multi-tenant/pull/152)
9
+ * 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)
10
+ * 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)
11
+ * Support create_distributed_table, create_reference_table in schema.rb [#156](https://github.com/citusdata/activerecord-multi-tenant/pull/156)
12
+ * Add client and server sidekiq middleware to sidekiq middleware chain [#158](https://github.com/citusdata/activerecord-multi-tenant/pull/158)
13
+
3
14
  ## 2.0.0 2022-05-19
4
15
 
5
16
  * 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, &block) }
121
+ else
122
+ #{original_method_name}(*args, &block)
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.2'
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.2
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-26 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