activerecord-multi-tenant 1.2.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: fa2e595cc76e33a877613504121106791680118da0a0c4957ff1190e75e99c3a
4
- data.tar.gz: 8a232e3431462d5b8b3972f0825664c210cc51729b2e94d08c5529851365d7f8
3
+ metadata.gz: e198970d8d5a4db04c8e203e28021668c765fdde78f9bb9a7cd0e6c577bfbe0e
4
+ data.tar.gz: 6abb30aac520063dbef7ddc5689714ef1dd9eb691aa382498b30c8bbcf03844b
5
5
  SHA512:
6
- metadata.gz: a2df2315ca041de29d2e82786a33904a9ac1dedd3f1ee4a2e470822cff977e9f4548ab0d33bd6eefec8055b9fc74ff8db2fdcf4c93cba4992aaaa681dcd1a695
7
- data.tar.gz: 9da5eec93a1bcd33bff29ab1e0035022592802f512d27139608e92988a17365d3fe4cff0a73fc0f83e9513cea9dd2bf6233bb4deac9533320fb0884f7ef8b219
6
+ metadata.gz: 5010dbbd2a8036ad5a3adae79a49567a279a318931ffe832663dd2430f455d1895e26cca6d96456484b894cc241cca21fbab31aa301f9ab4ef0538a4266b4005
7
+ data.tar.gz: e94b7ee0f619b0763879efefbae3a69792e00e8b9f2a2661a82666d4c129decea132f20b44999177bdc0162dec0e9b3cac4f402d70a76fb14518e283f4441d6c
@@ -19,10 +19,12 @@ jobs:
19
19
  - '3.0'
20
20
  - '3.1'
21
21
  gemfile:
22
+ - rails_5.2.3
22
23
  - rails_5.2
23
24
  - rails_6.0
24
25
  - rails_6.1
25
26
  - rails_7.0
27
+ - active_record_5.2.3
26
28
  - active_record_5.2
27
29
  - active_record_6.0
28
30
  - active_record_6.1
@@ -47,6 +49,14 @@ jobs:
47
49
  gemfile: 'rails_5.2'
48
50
  - ruby: '3.1'
49
51
  gemfile: 'active_record_5.2'
52
+ - ruby: '3.0'
53
+ gemfile: 'rails_5.2.3'
54
+ - ruby: '3.0'
55
+ gemfile: 'active_record_5.2.3'
56
+ - ruby: '3.1'
57
+ gemfile: 'rails_5.2.3'
58
+ - ruby: '3.1'
59
+ gemfile: 'active_record_5.2.3'
50
60
  name: Ruby ${{ matrix.ruby }} / ${{ matrix.gemfile }} ${{ (matrix.prepared_statements && 'w/ prepared statements') || '' }}
51
61
  env:
52
62
  BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
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
+
11
+ ## 2.0.0 2022-05-19
12
+
13
+ * Replace RequestStore with CurrentAttributes [#139](https://github.com/citusdata/activerecord-multi-tenant/pull/139)
14
+ * Support changing table_name after calling multi_tenant [#128](https://github.com/citusdata/activerecord-multi-tenant/pull/128)
15
+ * Allow to use uuid as primary key on partition table [#112](https://github.com/citusdata/activerecord-multi-tenant/pull/112)
16
+ * Support latest Rails 5.2 [#145](https://github.com/citusdata/activerecord-multi-tenant/pull/145)
17
+ * Support optional: true for belongs_to [#147](https://github.com/citusdata/activerecord-multi-tenant/pull/147)
18
+
19
+
3
20
  ## 1.2.0 2022-03-29
4
21
 
5
22
  * Test Rails 7 & Ruby 3
@@ -15,8 +15,7 @@ Gem::Specification.new do |s|
15
15
  s.homepage = 'https://github.com/citusdata/activerecord-multi-tenant'
16
16
  s.license = 'MIT'
17
17
 
18
- s.add_runtime_dependency('request_store', '>= 1.0.5')
19
- s.add_dependency('rails','>= 4.2')
18
+ s.add_dependency 'rails', '>= 5.2'
20
19
 
21
20
  s.add_development_dependency 'rspec', '>= 3.0'
22
21
  s.add_development_dependency 'rspec-rails'
@@ -0,0 +1,16 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "activerecord", "~> 5.2.0", "< 5.2.4" # FIXME
7
+ gem "i18n", "~> 0.9.5"
8
+ gem "nokogiri", "~> 1.7.1"
9
+ gem "nio4r", "~> 2.3.1"
10
+ gem "sprockets", "~> 3.7.1"
11
+ gem "byebug", "~> 11.0"
12
+ gem "rake", "12.0.0"
13
+ gem "redis", "3.3.3"
14
+ gem "pry-byebug", "3.9.0"
15
+
16
+ gemspec path: "../"
@@ -3,7 +3,7 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "appraisal"
6
- gem "activerecord", "~> 5.2.0", "< 5.2.4" # FIXME
6
+ gem "activerecord", "~> 5.2.0"
7
7
  gem "i18n", "~> 0.9.5"
8
8
  gem "nokogiri", "~> 1.7.1"
9
9
  gem "nio4r", "~> 2.3.1"
@@ -0,0 +1,16 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "rails", "~> 5.2.0", "< 5.2.4" # FIXME
7
+ gem "i18n", "~> 0.9.5"
8
+ gem "nokogiri", "~> 1.7.1"
9
+ gem "nio4r", "~> 2.3.1"
10
+ gem "sprockets", "~> 3.7.1"
11
+ gem "byebug", "~> 11.0"
12
+ gem "rake", "12.0.0"
13
+ gem "redis", "3.3.3"
14
+ gem "pry-byebug", "3.9.0"
15
+
16
+ gemspec path: "../"
@@ -3,7 +3,7 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "appraisal"
6
- gem "rails", "~> 5.2.0", "< 5.2.4" # FIXME
6
+ gem "rails", "~> 5.2.0"
7
7
  gem "i18n", "~> 0.9.5"
8
8
  gem "nokogiri", "~> 1.7.1"
9
9
  gem "nio4r", "~> 2.3.1"
@@ -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,10 +3,16 @@ 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
- before_create -> { self.id ||= self.class.connection.select_value("SELECT nextval('" + [self.class.table_name, self.class.primary_key, 'seq'].join('_') + "'::regclass)") }
9
+ before_create -> do
10
+ if self.class.columns_hash[self.class.primary_key].type == :uuid
11
+ self.id ||= SecureRandom.uuid
12
+ else
13
+ self.id ||= self.class.connection.select_value("SELECT nextval('#{self.class.table_name}_#{self.class.primary_key}_seq'::regclass)")
14
+ end
15
+ end
10
16
  end
11
17
  else
12
18
  class << self
@@ -38,18 +44,18 @@ module MultiTenant
38
44
 
39
45
  def inherited(subclass)
40
46
  super
41
- MultiTenant.register_multi_tenant_model(subclass.table_name, subclass) if subclass.table_name
47
+ MultiTenant.register_multi_tenant_model(subclass)
42
48
  end
43
49
  end
44
50
 
45
- MultiTenant.register_multi_tenant_model(table_name, self) if table_name
51
+ MultiTenant.register_multi_tenant_model(self)
46
52
 
47
53
  @partition_key = options[:partition_key] || MultiTenant.partition_key(tenant_name)
48
54
  partition_key = @partition_key
49
55
 
50
56
  # Create an implicit belongs_to association only if tenant class exists
51
57
  if MultiTenant.tenant_klass_defined?(tenant_name)
52
- belongs_to tenant_name, **options.slice(:class_name, :inverse_of).merge(foreign_key: options[:partition_key])
58
+ belongs_to tenant_name, **options.slice(:class_name, :inverse_of, :optional).merge(foreign_key: options[:partition_key])
53
59
  end
54
60
 
55
61
  # New instances should have the tenant set
@@ -123,6 +129,20 @@ end
123
129
 
124
130
  ActiveSupport.on_load(:active_record) do |base|
125
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
126
146
  end
127
147
 
128
148
  class ActiveRecord::Associations::Association
@@ -1,6 +1,10 @@
1
- require 'request_store'
1
+ require 'active_support/current_attributes'
2
2
 
3
3
  module MultiTenant
4
+ class Current < ::ActiveSupport::CurrentAttributes
5
+ attribute :tenant
6
+ end
7
+
4
8
  def self.tenant_klass_defined?(tenant_name)
5
9
  !!tenant_name.to_s.classify.safe_constantize
6
10
  end
@@ -24,13 +28,23 @@ module MultiTenant
24
28
  def self.with_lock_workaround_enabled?; @@enable_with_lock_workaround; end
25
29
 
26
30
  # Registry that maps table names to models (used by the query rewriter)
27
- def self.register_multi_tenant_model(table_name, model_klass)
28
- @@multi_tenant_models ||= {}
29
- @@multi_tenant_models[table_name.to_s] = model_klass
31
+ def self.register_multi_tenant_model(model_klass)
32
+ @@multi_tenant_models ||= []
33
+ @@multi_tenant_models.push(model_klass)
34
+
35
+ remove_class_variable(:@@multi_tenant_model_table_names) if defined?(@@multi_tenant_model_table_names)
30
36
  end
37
+
31
38
  def self.multi_tenant_model_for_table(table_name)
32
- @@multi_tenant_models ||= {}
33
- @@multi_tenant_models[table_name.to_s]
39
+ @@multi_tenant_models ||= []
40
+
41
+ if !defined?(@@multi_tenant_model_table_names)
42
+ @@multi_tenant_model_table_names = @@multi_tenant_models.map { |model|
43
+ [model.table_name, model] if model.table_name
44
+ }.compact.to_h
45
+ end
46
+
47
+ @@multi_tenant_model_table_names[table_name.to_s]
34
48
  end
35
49
 
36
50
  def self.multi_tenant_model_for_arel(arel)
@@ -43,11 +57,11 @@ module MultiTenant
43
57
  end
44
58
 
45
59
  def self.current_tenant=(tenant)
46
- RequestStore.store[:current_tenant] = tenant
60
+ Current.tenant = tenant
47
61
  end
48
62
 
49
63
  def self.current_tenant
50
- RequestStore.store[:current_tenant]
64
+ Current.tenant
51
65
  end
52
66
 
53
67
  def self.current_tenant_id
@@ -95,6 +109,23 @@ module MultiTenant
95
109
  end
96
110
  end
97
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
+
98
129
  # Preserve backward compatibility for people using .with_id
99
130
  singleton_class.send(:alias_method, :with_id, :with)
100
131
 
@@ -295,12 +295,9 @@ module ActiveRecord
295
295
  end
296
296
 
297
297
  node_list.select{ |n| n.is_a? Arel::Nodes::Join }.each do |node_join|
298
- if (!node_join.right ||
299
- (ActiveRecord::VERSION::MAJOR == 5 &&
300
- !node_join.right.expr.right.is_a?(Arel::Attributes::Attribute)))
298
+ if !node_join.right
301
299
  next
302
300
  end
303
-
304
301
  relation_right, relation_left = relations_from_node_join(node_join)
305
302
 
306
303
  next unless relation_right && relation_left
@@ -308,7 +305,7 @@ module ActiveRecord
308
305
  model_right = MultiTenant.multi_tenant_model_for_table(relation_left.table_name)
309
306
  model_left = MultiTenant.multi_tenant_model_for_table(relation_right.table_name)
310
307
  if model_right && model_left
311
- 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)
312
309
  node_join.right.expr = node_join.right.expr.and(join_enforcement_clause)
313
310
  end
314
311
  end
@@ -322,13 +319,13 @@ module ActiveRecord
322
319
 
323
320
  private
324
321
  def relations_from_node_join(node_join)
325
- if ActiveRecord::VERSION::MAJOR == 5 || node_join.right.expr.is_a?(Arel::Nodes::Equality)
322
+ if node_join.right.expr.is_a?(Arel::Nodes::Equality)
326
323
  return node_join.right.expr.right.relation, node_join.right.expr.left.relation
327
324
  end
328
325
 
329
- children = node_join.right.expr.children
326
+ children = [node_join.right.expr.children].flatten
330
327
 
331
- tenant_applied = children.any?(MultiTenant::TenantEnforcementClause) || children.any?(MultiTenant::TenantJoinEnforcementClause)
328
+ tenant_applied = children.any?{|c| c.is_a?(MultiTenant::TenantEnforcementClause) || c.is_a?(MultiTenant::TenantJoinEnforcementClause)}
332
329
  if tenant_applied || children.empty?
333
330
  return nil, nil
334
331
  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 = '1.2.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,65 @@ 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
+
91
+ describe 'Changes table_name after multi_tenant called' do
92
+ before do
93
+ account_klass.has_many(:posts, anonymous_class: post_klass)
94
+ post_klass.belongs_to(:account, anonymous_class: account_klass)
95
+
96
+ @account1 = account_klass.create! name: 'foo'
97
+ @account2 = account_klass.create! name: 'bar'
98
+
99
+ @post1 = @account1.posts.create! name: 'foobar'
100
+ @post2 = @account2.posts.create! name: 'baz'
101
+
102
+ MultiTenant.current_tenant = @account1
103
+ @posts = post_klass.all
104
+ end
105
+
106
+ let(:account_klass) do
107
+ Class.new(Account) do
108
+ def self.name
109
+ 'Account'
110
+ end
111
+ end
112
+ end
113
+
114
+ let(:post_klass) do
115
+ Class.new(ActiveRecord::Base) do
116
+ self.table_name = 'unknown'
117
+
118
+ multi_tenant(:account)
119
+
120
+ self.table_name = 'posts'
121
+
122
+ def self.name
123
+ 'Post'
124
+ end
125
+ end
126
+ end
127
+
128
+ it { expect(@posts.length).to eq(1) }
129
+ it { expect(@posts).to eq([@post1]) }
130
+ end
131
+
73
132
  # Scoping models
74
133
  describe 'Project.all should be scoped to the current tenant if set' do
75
134
  before do
@@ -144,6 +203,12 @@ describe MultiTenant do
144
203
  end
145
204
  end
146
205
 
206
+ it 'handles belongs_to with optional: true' do
207
+ record = OptionalSubTask.create(sub_task_id: sub_task.id)
208
+ expect(record.reload.sub_task).to eq(sub_task)
209
+ expect(record.account_id).to eq(nil)
210
+ end
211
+
147
212
  it 'handles has_many through' do
148
213
  MultiTenant.with(account) do
149
214
  expect(project.sub_tasks).to eq [sub_task]
@@ -349,10 +414,10 @@ describe MultiTenant do
349
414
 
350
415
  it "applies the team_id conditions in the where clause" do
351
416
  option1 = <<-sql.strip
352
- 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
353
418
  sql
354
419
  option2 = <<-sql.strip
355
- 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
356
421
  sql
357
422
 
358
423
  account1 = Account.create! name: 'Account 1'
@@ -367,7 +432,7 @@ describe MultiTenant do
367
432
 
368
433
  MultiTenant.without do
369
434
  expected_sql = <<-sql
370
- 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
371
436
  sql
372
437
 
373
438
  project = Project.first
@@ -405,7 +470,7 @@ describe MultiTenant do
405
470
  expect(project.categories).to include(category1)
406
471
 
407
472
  expected_sql = <<-sql
408
- 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
409
474
  sql
410
475
 
411
476
  expect(Project.where(account_id: 1).joins(:categories).to_sql).to eq(expected_sql.strip)
@@ -439,7 +504,7 @@ describe MultiTenant do
439
504
 
440
505
  MultiTenant.without do
441
506
  expected_sql = <<-sql
442
- 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
443
508
  sql
444
509
 
445
510
  expect(Project.where(account_id: 1).eager_load(:categories).to_sql).to eq(expected_sql.strip)
@@ -471,7 +536,7 @@ describe MultiTenant do
471
536
 
472
537
  MultiTenant.without do
473
538
  expected_sql = <<-sql
474
- 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
475
540
  sql
476
541
 
477
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
@@ -34,6 +34,13 @@ ARGV.grep(/\w+_spec\.rb/).empty? && ActiveRecord::Schema.define(version: 1) do
34
34
  t.column :type, :string
35
35
  end
36
36
 
37
+ create_table :optional_sub_tasks, force: true do |t|
38
+ t.references :account, :integer
39
+ t.column :sub_task_id, :integer
40
+ t.column :name, :string
41
+ t.column :type, :string
42
+ end
43
+
37
44
  create_table :countries, force: true do |t|
38
45
  t.column :name, :string
39
46
  end
@@ -106,6 +113,11 @@ ARGV.grep(/\w+_spec\.rb/).empty? && ActiveRecord::Schema.define(version: 1) do
106
113
  t.column :domain_id, :integer
107
114
  end
108
115
 
116
+ create_table :posts, force: true, partition_key: :account_id do |t|
117
+ t.column :account_id, :integer
118
+ t.column :name, :string
119
+ end
120
+
109
121
  create_distributed_table :accounts, :id
110
122
  create_distributed_table :projects, :account_id
111
123
  create_distributed_table :managers, :account_id
@@ -121,6 +133,7 @@ ARGV.grep(/\w+_spec\.rb/).empty? && ActiveRecord::Schema.define(version: 1) do
121
133
  create_distributed_table :allowed_places, :account_id
122
134
  create_distributed_table :domains, :account_id
123
135
  create_distributed_table :pages, :account_id
136
+ create_distributed_table :posts, :account_id
124
137
  create_reference_table :categories
125
138
  end
126
139
 
@@ -128,6 +141,7 @@ class Account < ActiveRecord::Base
128
141
  multi_tenant :account
129
142
  has_many :projects
130
143
  has_one :manager, inverse_of: :account
144
+ has_many :optional_sub_tasks
131
145
  end
132
146
 
133
147
  class Project < ActiveRecord::Base
@@ -159,6 +173,14 @@ class SubTask < ActiveRecord::Base
159
173
  multi_tenant :account
160
174
  belongs_to :task
161
175
  has_one :project, through: :task
176
+ has_many :optional_sub_tasks
177
+ end
178
+
179
+ with_belongs_to_required_by_default do
180
+ class OptionalSubTask < ActiveRecord::Base
181
+ multi_tenant :account, optional: true
182
+ belongs_to :sub_task
183
+ end
162
184
  end
163
185
 
164
186
  class StiSubTask < SubTask
@@ -198,6 +220,7 @@ class Comment < ActiveRecord::Base
198
220
  end
199
221
 
200
222
  class Organization < ActiveRecord::Base
223
+ multi_tenant :organization
201
224
  has_many :uuid_records
202
225
  end
203
226
 
data/spec/spec_helper.rb CHANGED
@@ -46,4 +46,11 @@ def uses_prepared_statements?
46
46
  ActiveRecord::Base.connection.prepared_statements
47
47
  end
48
48
 
49
+ def with_belongs_to_required_by_default(&block)
50
+ default_value = ActiveRecord::Base.belongs_to_required_by_default
51
+ ActiveRecord::Base.belongs_to_required_by_default = true
52
+ yield
53
+ ensure
54
+ ActiveRecord::Base.belongs_to_required_by_default = default_value
55
+ end
49
56
  require 'schema'
metadata CHANGED
@@ -1,43 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-multi-tenant
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.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-04-11 00:00:00.000000000 Z
11
+ date: 2022-10-20 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: request_store
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: 1.0.5
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: 1.0.5
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: rails
29
15
  requirement: !ruby/object:Gem::Requirement
30
16
  requirements:
31
17
  - - ">="
32
18
  - !ruby/object:Gem::Version
33
- version: '4.2'
19
+ version: '5.2'
34
20
  type: :runtime
35
21
  prerelease: false
36
22
  version_requirements: !ruby/object:Gem::Requirement
37
23
  requirements:
38
24
  - - ">="
39
25
  - !ruby/object:Gem::Version
40
- version: '4.2'
26
+ version: '5.2'
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: rspec
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -168,10 +154,12 @@ files:
168
154
  - activerecord-multi-tenant.gemspec
169
155
  - docker-compose.yml
170
156
  - gemfiles/.bundle/config
157
+ - gemfiles/active_record_5.2.3.gemfile
171
158
  - gemfiles/active_record_5.2.gemfile
172
159
  - gemfiles/active_record_6.0.gemfile
173
160
  - gemfiles/active_record_6.1.gemfile
174
161
  - gemfiles/active_record_7.0.gemfile
162
+ - gemfiles/rails_5.2.3.gemfile
175
163
  - gemfiles/rails_5.2.gemfile
176
164
  - gemfiles/rails_6.0.gemfile
177
165
  - gemfiles/rails_6.1.gemfile
@@ -184,12 +172,12 @@ files:
184
172
  - lib/activerecord-multi-tenant/migrations.rb
185
173
  - lib/activerecord-multi-tenant/model_extensions.rb
186
174
  - lib/activerecord-multi-tenant/multi_tenant.rb
187
- - lib/activerecord-multi-tenant/persistence_extension.rb
188
175
  - lib/activerecord-multi-tenant/query_monitor.rb
189
176
  - lib/activerecord-multi-tenant/query_rewriter.rb
190
177
  - lib/activerecord-multi-tenant/sidekiq.rb
191
178
  - lib/activerecord-multi-tenant/version.rb
192
179
  - lib/activerecord-multi-tenant/with_lock.rb
180
+ - spec/activerecord-multi-tenant/associations_spec.rb
193
181
  - spec/activerecord-multi-tenant/controller_extensions_spec.rb
194
182
  - spec/activerecord-multi-tenant/fast_truncate_spec.rb
195
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