activerecord-multi-tenant 2.0.0 → 2.2.0

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: 15ad19bf20781129dc1bd57d4e4b8eb8731044cea3b89f29a62789d0f5a45aee
4
+ data.tar.gz: 0f76290b00a7d540495972ab5a9de09d3abbb1dde11758d58b08493bf743b5da
5
5
  SHA512:
6
- metadata.gz: 7715dadcbd9cd13d86135518d3fc62283d9ab85e802016ab1be15fe39d815ec344c1d3e4699f96870b6e50b6d3b66b4a38f780d07ad6d3d4b8e21668deacca18
7
- data.tar.gz: d1d364ebde87012cc5b91a6cf812a1a8cc0f89bb5ee8859b3de01ab0392dc5a62b74a3f93ac721d3e583343b703fc872d73c1f301cff9716e686492f06922d36
6
+ metadata.gz: 17f7b912ecc314e5462affd74daf2e1b79239f8f65bfa30c8896f4acb9e1d787c240aeefe8cb09045b34ab2772d46aab4c42bb8cdb3374df247f1131a08758d0
7
+ data.tar.gz: 713df8fdefb6cd50b2c02408a9763da39e532b957d176fe5a8a6e1fbe265d2cf79457ae4d196d0e6ddfdf8effe0ca370c0297ea7186a5ce145717e4a93772be1
@@ -13,50 +13,24 @@ jobs:
13
13
  fail-fast: false
14
14
  matrix:
15
15
  ruby:
16
- - '2.5'
17
16
  - '2.6'
18
17
  - '2.7'
19
18
  - '3.0'
20
19
  - '3.1'
21
20
  gemfile:
22
- - rails_5.2.3
23
- - rails_5.2
24
21
  - rails_6.0
25
22
  - rails_6.1
26
23
  - rails_7.0
27
- - active_record_5.2.3
28
- - active_record_5.2
29
24
  - active_record_6.0
30
25
  - active_record_6.1
31
26
  - active_record_7.0
32
27
  prepared_statements: [true, false]
33
28
  exclude:
34
29
  # activesupport-7.0.0 requires ruby version >= 2.7.0
35
- - ruby: '2.5'
36
- gemfile: 'rails_7.0'
37
- - ruby: '2.5'
38
- gemfile: 'active_record_7.0'
39
30
  - ruby: '2.6'
40
31
  gemfile: 'rails_7.0'
41
32
  - ruby: '2.6'
42
33
  gemfile: 'active_record_7.0'
43
- # ruby >3 and activesupport 5.2 are not compatible
44
- - ruby: '3.0'
45
- gemfile: 'rails_5.2'
46
- - ruby: '3.0'
47
- gemfile: 'active_record_5.2'
48
- - ruby: '3.1'
49
- gemfile: 'rails_5.2'
50
- - ruby: '3.1'
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'
60
34
  name: Ruby ${{ matrix.ruby }} / ${{ matrix.gemfile }} ${{ (matrix.prepared_statements && 'w/ prepared statements') || '' }}
61
35
  env:
62
36
  BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
data/Appraisals CHANGED
@@ -1,15 +1,3 @@
1
- appraise 'rails-5.2' do
2
- gem 'rails', '~> 5.2.0'
3
- gem 'i18n', '~> 0.9.5'
4
- gem 'nokogiri', '~> 1.7.1'
5
- gem 'nio4r', '~> 2.3.1'
6
- gem 'sprockets', '~> 3.7.1'
7
- gem 'byebug', '~> 11.0'
8
- gem 'rake', '12.0.0'
9
- gem 'redis', '3.3.3'
10
- gem 'pry-byebug', '3.9.0'
11
- end
12
-
13
1
  appraise 'rails-6.0' do
14
2
  gem 'rails', '~> 6.0.3'
15
3
  end
@@ -22,18 +10,6 @@ appraise 'rails-7.0' do
22
10
  gem 'rails', '~> 7.0.0'
23
11
  end
24
12
 
25
- appraise 'active-record-5.2' do
26
- gem 'activerecord', '~> 5.2.0'
27
- gem 'i18n', '~> 0.9.5'
28
- gem 'nokogiri', '~> 1.7.1'
29
- gem 'nio4r', '~> 2.3.1'
30
- gem 'sprockets', '~> 3.7.1'
31
- gem 'byebug', '~> 11.0'
32
- gem 'rake', '12.0.0'
33
- gem 'redis', '3.3.3'
34
- gem 'pry-byebug', '3.9.0'
35
- end
36
-
37
13
  appraise 'active-record-6.0' do
38
14
  gem 'activerecord', '~> 6.0.3'
39
15
  end
data/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.2.0 2022-12-06
4
+ * Handle changing tenant from `nil` to a value [#173](https://github.com/citusdata/activerecord-multi-tenant/pull/173)
5
+ * Allow Partitioned tables to be created without a primary key [#172](https://github.com/citusdata/activerecord-multi-tenant/pull/172)
6
+ * Only attempt to reload with MultiTenant when parition_key is present [#175](https://github.com/citusdata/activerecord-multi-tenant/pull/175)
7
+ * Remove support for Ruby 2.5 & ActiveRecord 5.2
8
+
9
+ ## 2.1.6 2022-11-23
10
+ * Fix undefined wrap_methods error & wrap_methods version check [#170](https://github.com/citusdata/activerecord-multi-tenant/pull/170)
11
+
12
+ ## 2.1.5 2022-11-20
13
+ * Fix `MultiTenant.without` codegen bug in Rails 6.1+ [#168](https://github.com/citusdata/activerecord-multi-tenant/pull/168)
14
+
15
+ ## 2.1.4 2022-11-03
16
+ * Fixes #166 where db:schema:dump is broken when using this gem with MySQL [#167](https://github.com/citusdata/activerecord-multi-tenant/pull/167)
17
+
18
+ ## 2.1.3 2022-10-27
19
+ * Error when calling a method that takes keyword arguments with MultiTenant.wrap_methods [#164](https://github.com/citusdata/activerecord-multi-tenant/pull/164)
20
+
21
+ ## 2.1.2 2022-10-26
22
+ * Fixes issue when wraping methods that require a block [#162](https://github.com/citusdata/activerecord-multi-tenant/pull/162)
23
+
24
+ ## 2.1.1 2022-10-20
25
+ * Fix query building for models with mismatched partition_keys [#150](https://github.com/citusdata/activerecord-multi-tenant/pull/150)
26
+ * Identify tenant even if class name is nonstandard [#152](https://github.com/citusdata/activerecord-multi-tenant/pull/152)
27
+ * 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)
28
+ * 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)
29
+ * Support create_distributed_table, create_reference_table in schema.rb [#156](https://github.com/citusdata/activerecord-multi-tenant/pull/156)
30
+ * Add client and server sidekiq middleware to sidekiq middleware chain [#158](https://github.com/citusdata/activerecord-multi-tenant/pull/158)
31
+
3
32
  ## 2.0.0 2022-05-19
4
33
 
5
34
  * Replace RequestStore with CurrentAttributes [#139](https://github.com/citusdata/activerecord-multi-tenant/pull/139)
data/README.md CHANGED
@@ -16,7 +16,7 @@ gem 'activerecord-multi-tenant'
16
16
 
17
17
  ## Supported Rails versions
18
18
 
19
- All Ruby on Rails versions starting with 4.2 or newer (up to 6.0) are supported.
19
+ All Ruby on Rails versions starting with 6.0 or newer (up to 7.0) are supported.
20
20
 
21
21
  This gem only supports ActiveRecord (the Rails default ORM), and not alternative ORMs like Sequel.
22
22
 
@@ -15,7 +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_dependency 'rails', '>= 5.2'
18
+ s.add_dependency 'rails', '>= 6'
19
19
 
20
20
  s.add_development_dependency 'rspec', '>= 3.0'
21
21
  s.add_development_dependency 'rspec-rails'
@@ -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,24 +56,22 @@ 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
- if options[:partition_key] && options[:partition_key].to_s != 'id'
74
+ if options[:id] != false && options[:partition_key] && options[:partition_key].to_s != 'id'
49
75
  execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{table_name}_pkey"
50
76
  execute "ALTER TABLE #{table_name} ADD PRIMARY KEY(\"#{options[:partition_key]}\", id)"
51
77
  end
@@ -54,3 +80,44 @@ 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
+ citus_version = begin
93
+ ActiveRecord::Migration.citus_version
94
+ rescue StandardError
95
+ # Handle the case where this gem is used with MySQL https://github.com/citusdata/activerecord-multi-tenant/issues/166
96
+ nil
97
+ end
98
+ @distribution_columns =
99
+ if citus_version.present?
100
+ @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|
101
+ [v['table_name'], v['dist_col_name']]
102
+ end
103
+ else
104
+ {}
105
+ end
106
+ end
107
+
108
+ # Support for create_distributed_table & create_reference_table
109
+ alias table_without_citus table
110
+ def table(table, stream)
111
+ table_without_citus(table, stream)
112
+ table_name = remove_prefix_and_suffix(table)
113
+ distribution_column = @distribution_columns[table_name]
114
+ if distribution_column
115
+ stream.puts " create_distributed_table(#{table_name.inspect}, #{distribution_column.inspect})"
116
+ stream.puts
117
+ elsif @distribution_columns.key?(table_name)
118
+ stream.puts " create_reference_table(#{table_name.inspect})"
119
+ stream.puts
120
+ end
121
+ end
122
+ end
123
+ end
@@ -1,9 +1,11 @@
1
+ require_relative './multi_tenant'
2
+
1
3
  module MultiTenant
2
4
  module ModelExtensionsClassMethods
3
5
  DEFAULT_ID_FIELD = 'id'.freeze
4
6
 
5
7
  def multi_tenant(tenant_name, options = {})
6
- if to_s.underscore.to_sym == tenant_name
8
+ if to_s.underscore.to_sym == tenant_name || (!table_name.nil? && table_name.singularize.to_sym == tenant_name)
7
9
  unless MultiTenant.with_write_only_mode_enabled?
8
10
  # This is the tenant model itself. Workaround for https://github.com/citusdata/citus/issues/687
9
11
  before_create -> do
@@ -73,7 +75,6 @@ module MultiTenant
73
75
  # Rails 5 `attribute_will_change!` uses the attribute-method-call rather than `read_attribute`
74
76
  # and will raise ActiveModel::MissingAttributeError if that column was not selected.
75
77
  # This is rescued as NoMethodError and in MRI attribute_was is assigned an arbitrary Object
76
- # This is still true after the Rails 5.2 refactor
77
78
  was = send("#{partition_key}_was")
78
79
  was_nil_or_skipped = was.nil? || was.class == Object
79
80
 
@@ -100,7 +101,8 @@ module MultiTenant
100
101
  include to_include
101
102
 
102
103
  around_save -> (record, block) {
103
- if persisted? && MultiTenant.current_tenant_id.nil?
104
+ record_tenant = record.attribute_was(partition_key)
105
+ if persisted? && MultiTenant.current_tenant_id.nil? && !record_tenant.nil?
104
106
  MultiTenant.with(record.public_send(partition_key)) { block.call }
105
107
  else
106
108
  block.call
@@ -108,7 +110,8 @@ module MultiTenant
108
110
  }
109
111
 
110
112
  around_update -> (record, block) {
111
- if MultiTenant.current_tenant_id.nil?
113
+ record_tenant = record.attribute_was(partition_key)
114
+ if MultiTenant.current_tenant_id.nil? && !record_tenant.nil?
112
115
  MultiTenant.with(record.public_send(partition_key)) { block.call }
113
116
  else
114
117
  block.call
@@ -129,6 +132,20 @@ end
129
132
 
130
133
  ActiveSupport.on_load(:active_record) do |base|
131
134
  base.extend MultiTenant::ModelExtensionsClassMethods
135
+
136
+ # Ensure we have current_tenant_id in where clause when a cached ActiveRecord instance is being reloaded, or update_columns without callbacks is called
137
+ MultiTenant.wrap_methods(ActiveRecord::Base, 'self', :delete, :reload, :update_columns)
138
+
139
+ # Any queuries fired for fetching a singular association have the correct current_tenant_id in WHERE clause
140
+ # reload is called anytime any record's association is accessed
141
+ MultiTenant.wrap_methods(ActiveRecord::Associations::Association, 'owner', :reload)
142
+
143
+ # 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
144
+ ActiveRecord::Associations::CollectionProxy.alias_method :equals_mt, :== # Hack to prevent syntax error due to invalid method name
145
+ ActiveRecord::Associations::CollectionProxy.alias_method :append_mt, :<< # Hack to prevent syntax error due to invalid method name
146
+ 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)
147
+ ActiveRecord::Associations::CollectionProxy.alias_method :==, :equals_mt
148
+ ActiveRecord::Associations::CollectionProxy.alias_method :<<, :append_mt
132
149
  end
133
150
 
134
151
  class ActiveRecord::Associations::Association
@@ -109,6 +109,41 @@ 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
+ if Gem::Version.create(RUBY_VERSION) < Gem::Version.new('3.0.0')
114
+ def self.wrap_methods(klass, owner, *method_names)
115
+ method_names.each do |method_name|
116
+ original_method_name = :"_mt_original_#{method_name}"
117
+ klass.class_eval <<-CODE, __FILE__, __LINE__ + 1
118
+ alias_method :#{original_method_name}, :#{method_name}
119
+ def #{method_name}(*args, &block)
120
+ if MultiTenant.multi_tenant_model_for_table(#{owner}.class.table_name).present? && #{owner}.persisted? && MultiTenant.current_tenant_id.nil? && #{owner}.class.respond_to?(:partition_key) && #{owner}.attributes.include?(#{owner}.class.partition_key)
121
+ MultiTenant.with(#{owner}.public_send(#{owner}.class.partition_key)) { #{original_method_name}(*args, &block) }
122
+ else
123
+ #{original_method_name}(*args, &block)
124
+ end
125
+ end
126
+ CODE
127
+ end
128
+ end
129
+ else
130
+ def self.wrap_methods(klass, owner, *method_names)
131
+ method_names.each do |method_name|
132
+ original_method_name = :"_mt_original_#{method_name}"
133
+ klass.class_eval <<-CODE, __FILE__, __LINE__ + 1
134
+ alias_method :#{original_method_name}, :#{method_name}
135
+ def #{method_name}(...)
136
+ if MultiTenant.multi_tenant_model_for_table(#{owner}.class.table_name).present? && #{owner}.persisted? && MultiTenant.current_tenant_id.nil? && #{owner}.class.respond_to?(:partition_key) && #{owner}.attributes.include?(#{owner}.class.partition_key)
137
+ MultiTenant.with(#{owner}.public_send(#{owner}.class.partition_key)) { #{original_method_name}(...) }
138
+ else
139
+ #{original_method_name}(...)
140
+ end
141
+ end
142
+ CODE
143
+ end
144
+ end
145
+ end
146
+
112
147
  # Preserve backward compatibility for people using .with_id
113
148
  singleton_class.send(:alias_method, :with_id, :with)
114
149
 
@@ -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
@@ -330,8 +330,9 @@ module ActiveRecord
330
330
  return nil, nil
331
331
  end
332
332
 
333
- if children[0].right.respond_to?('relation') && children[0].left.respond_to?('relation')
334
- return children[0].right.relation, children[0].left.relation
333
+ child = children.first.respond_to?(:children) ? children.first.children.first : children.first
334
+ if child.right.respond_to?(:relation) && child.left.respond_to?(:relation)
335
+ return child.right.relation, child.left.relation
335
336
  end
336
337
 
337
338
  return nil, nil
@@ -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.2.0'
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,13 +204,19 @@ 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
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
192
211
 
193
- record = sub_task.optional_sub_tasks.create!
212
+ it 'handles changing tenant from nil to a value' do
213
+ record = OptionalSubTask.create(sub_task_id: sub_task.id)
194
214
  expect(record.reload.sub_task).to eq(sub_task)
195
215
  expect(record.account_id).to eq(nil)
216
+
217
+ record.account = account
218
+ record.save!
219
+ expect(record.reload.account_id).to eq(account.id)
196
220
  end
197
221
 
198
222
  it 'handles has_many through' do
@@ -285,6 +309,55 @@ describe MultiTenant do
285
309
  end
286
310
  end
287
311
 
312
+ # Joins
313
+ describe 'joins for models' do
314
+ context 'for models with where condition in associations' do
315
+ let(:account) { Account.create!(name: 'Account 1') }
316
+
317
+ it 'should add tenant condition to the queries when tenant is set' do
318
+ expected_join_sql = <<-SQL.strip
319
+ SELECT "comments".* FROM "comments" INNER JOIN "tasks" ON "tasks"."id" = "comments"."commentable_id" AND "comments"."commentable_type" = 'Task' AND "tasks"."account_id" = 1 WHERE "comments"."account_id" = 1
320
+ SQL
321
+
322
+ MultiTenant.with(account) do
323
+ expect(Comment.joins(:task).to_sql).to eq(expected_join_sql)
324
+ end
325
+ end
326
+
327
+ it 'should add tenant condition to the queries when tenant is not set' do
328
+ MultiTenant.without do
329
+ expected_join_sql = <<-SQL.strip
330
+ SELECT "comments".* FROM "comments" INNER JOIN "tasks" ON "tasks"."id" = "comments"."commentable_id" AND "comments"."commentable_type" = 'Task' AND "comments"."account_id" = "tasks"."account_id"
331
+ SQL
332
+ expect(Comment.joins(:task).to_sql).to eq(expected_join_sql)
333
+ end
334
+ end
335
+ end
336
+
337
+ context 'for models with default associations' do
338
+ let(:account) { Account.create!(name: 'Account 1') }
339
+
340
+ it 'should add tenant condition to the queries when tenant is set' do
341
+ expected_join_sql = <<-SQL.strip
342
+ SELECT "projects".* FROM "projects" INNER JOIN "tasks" ON "tasks"."project_id" = "projects"."id" AND "tasks"."account_id" = 1 WHERE "projects"."account_id" = 1
343
+ SQL
344
+
345
+ MultiTenant.with(account) do
346
+ expect(Project.joins(:tasks).to_sql).to eq(expected_join_sql)
347
+ end
348
+ end
349
+
350
+ it 'should add tenant condition to the queries when tenant is not set' do
351
+ MultiTenant.without do
352
+ expected_join_sql = <<-SQL.strip
353
+ SELECT "projects".* FROM "projects" INNER JOIN "tasks" ON "tasks"."project_id" = "projects"."id" AND "projects"."account_id" = "tasks"."account_id"
354
+ SQL
355
+ expect(Project.joins(:tasks).to_sql).to eq(expected_join_sql)
356
+ end
357
+ end
358
+ end
359
+ end
360
+
288
361
  # ::with
289
362
  describe "::with" do
290
363
  it "should set current_tenant to the specified tenant inside the block" do
@@ -400,10 +473,10 @@ describe MultiTenant do
400
473
 
401
474
  it "applies the team_id conditions in the where clause" do
402
475
  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
476
+ 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
477
  sql
405
478
  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
479
+ 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
480
  sql
408
481
 
409
482
  account1 = Account.create! name: 'Account 1'
@@ -418,7 +491,7 @@ describe MultiTenant do
418
491
 
419
492
  MultiTenant.without do
420
493
  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
494
+ 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
495
  sql
423
496
 
424
497
  project = Project.first
@@ -456,7 +529,7 @@ describe MultiTenant do
456
529
  expect(project.categories).to include(category1)
457
530
 
458
531
  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
532
+ 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
533
  sql
461
534
 
462
535
  expect(Project.where(account_id: 1).joins(:categories).to_sql).to eq(expected_sql.strip)
@@ -490,7 +563,7 @@ describe MultiTenant do
490
563
 
491
564
  MultiTenant.without do
492
565
  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
566
+ 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
567
  sql
495
568
 
496
569
  expect(Project.where(account_id: 1).eager_load(:categories).to_sql).to eq(expected_sql.strip)
@@ -522,7 +595,7 @@ describe MultiTenant do
522
595
 
523
596
  MultiTenant.without do
524
597
  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
598
+ 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
599
  sql
527
600
 
528
601
  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.2.0
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-12-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '5.2'
19
+ version: '6'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '5.2'
26
+ version: '6'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rspec
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -154,13 +154,9 @@ files:
154
154
  - activerecord-multi-tenant.gemspec
155
155
  - docker-compose.yml
156
156
  - gemfiles/.bundle/config
157
- - gemfiles/active_record_5.2.3.gemfile
158
- - gemfiles/active_record_5.2.gemfile
159
157
  - gemfiles/active_record_6.0.gemfile
160
158
  - gemfiles/active_record_6.1.gemfile
161
159
  - gemfiles/active_record_7.0.gemfile
162
- - gemfiles/rails_5.2.3.gemfile
163
- - gemfiles/rails_5.2.gemfile
164
160
  - gemfiles/rails_6.0.gemfile
165
161
  - gemfiles/rails_6.1.gemfile
166
162
  - gemfiles/rails_7.0.gemfile
@@ -172,12 +168,12 @@ files:
172
168
  - lib/activerecord-multi-tenant/migrations.rb
173
169
  - lib/activerecord-multi-tenant/model_extensions.rb
174
170
  - lib/activerecord-multi-tenant/multi_tenant.rb
175
- - lib/activerecord-multi-tenant/persistence_extension.rb
176
171
  - lib/activerecord-multi-tenant/query_monitor.rb
177
172
  - lib/activerecord-multi-tenant/query_rewriter.rb
178
173
  - lib/activerecord-multi-tenant/sidekiq.rb
179
174
  - lib/activerecord-multi-tenant/version.rb
180
175
  - lib/activerecord-multi-tenant/with_lock.rb
176
+ - spec/activerecord-multi-tenant/associations_spec.rb
181
177
  - spec/activerecord-multi-tenant/controller_extensions_spec.rb
182
178
  - spec/activerecord-multi-tenant/fast_truncate_spec.rb
183
179
  - spec/activerecord-multi-tenant/model_extensions_spec.rb
@@ -1,16 +0,0 @@
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: "../"
@@ -1,16 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "appraisal"
6
- gem "activerecord", "~> 5.2.0"
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: "../"
@@ -1,16 +0,0 @@
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: "../"
@@ -1,16 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "appraisal"
6
- gem "rails", "~> 5.2.0"
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: "../"
@@ -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