activerecord-multi-tenant 1.1.1 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/CI.yml +47 -0
  3. data/.gitignore +2 -0
  4. data/.rspec +1 -0
  5. data/Appraisals +6 -22
  6. data/CHANGELOG.md +46 -0
  7. data/README.md +1 -1
  8. data/activerecord-multi-tenant.gemspec +1 -2
  9. data/gemfiles/active_record_7.0.gemfile +8 -0
  10. data/gemfiles/rails_7.0.gemfile +8 -0
  11. data/lib/activerecord-multi-tenant/copy_from_client.rb +2 -2
  12. data/lib/activerecord-multi-tenant/migrations.rb +76 -9
  13. data/lib/activerecord-multi-tenant/model_extensions.rb +31 -8
  14. data/lib/activerecord-multi-tenant/multi_tenant.rb +57 -8
  15. data/lib/activerecord-multi-tenant/query_rewriter.rb +15 -11
  16. data/lib/activerecord-multi-tenant/sidekiq.rb +21 -1
  17. data/lib/activerecord-multi-tenant/version.rb +1 -1
  18. data/lib/activerecord-multi-tenant.rb +0 -1
  19. data/spec/activerecord-multi-tenant/associations_spec.rb +21 -0
  20. data/spec/activerecord-multi-tenant/model_extensions_spec.rb +186 -54
  21. data/spec/activerecord-multi-tenant/query_rewriter_spec.rb +17 -0
  22. data/spec/activerecord-multi-tenant/record_modifications_spec.rb +19 -0
  23. data/spec/activerecord-multi-tenant/sidekiq_spec.rb +11 -0
  24. data/spec/schema.rb +50 -4
  25. data/spec/spec_helper.rb +7 -0
  26. metadata +13 -33
  27. data/.travis.yml +0 -34
  28. data/Gemfile.lock +0 -199
  29. data/gemfiles/active_record_5.2.gemfile +0 -16
  30. data/gemfiles/active_record_5.2.gemfile.lock +0 -188
  31. data/gemfiles/active_record_6.0.gemfile.lock +0 -198
  32. data/gemfiles/active_record_6.1.gemfile.lock +0 -198
  33. data/gemfiles/rails_5.2.gemfile +0 -16
  34. data/gemfiles/rails_5.2.gemfile.lock +0 -188
  35. data/gemfiles/rails_6.0.gemfile.lock +0 -198
  36. data/gemfiles/rails_6.1.gemfile.lock +0 -198
  37. data/lib/activerecord-multi-tenant/persistence_extension.rb +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 79f35cfc24d053c192a889fcb387c764cf2580e350aa8c7db5ff4ea84d3c44df
4
- data.tar.gz: 04bb2200ae11168efe55090482686b00e457f267696d110fec5669ae1e2c70fd
3
+ metadata.gz: 15ad19bf20781129dc1bd57d4e4b8eb8731044cea3b89f29a62789d0f5a45aee
4
+ data.tar.gz: 0f76290b00a7d540495972ab5a9de09d3abbb1dde11758d58b08493bf743b5da
5
5
  SHA512:
6
- metadata.gz: 5b807cc7064efd06487fa8995f49cfbfdc7b15e5ccbc1e84510e0c742d6b14f8e7168145e6a520c8ea613735621b799dc6478ed8a749db68c7020ebe936b73b1
7
- data.tar.gz: 4e8aca6b32a413a54ecc900dc6e36ee57f06faf88504d4c4b210153456b5090aa8a2a660555c1587d1c1a06ae0126db3002e0cba0d0d935f0688f7bc84c9c362
6
+ metadata.gz: 17f7b912ecc314e5462affd74daf2e1b79239f8f65bfa30c8896f4acb9e1d787c240aeefe8cb09045b34ab2772d46aab4c42bb8cdb3374df247f1131a08758d0
7
+ data.tar.gz: 713df8fdefb6cd50b2c02408a9763da39e532b957d176fe5a8a6e1fbe265d2cf79457ae4d196d0e6ddfdf8effe0ca370c0297ea7186a5ce145717e4a93772be1
@@ -0,0 +1,47 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ pull_request:
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ ruby:
16
+ - '2.6'
17
+ - '2.7'
18
+ - '3.0'
19
+ - '3.1'
20
+ gemfile:
21
+ - rails_6.0
22
+ - rails_6.1
23
+ - rails_7.0
24
+ - active_record_6.0
25
+ - active_record_6.1
26
+ - active_record_7.0
27
+ prepared_statements: [true, false]
28
+ exclude:
29
+ # activesupport-7.0.0 requires ruby version >= 2.7.0
30
+ - ruby: '2.6'
31
+ gemfile: 'rails_7.0'
32
+ - ruby: '2.6'
33
+ gemfile: 'active_record_7.0'
34
+ name: Ruby ${{ matrix.ruby }} / ${{ matrix.gemfile }} ${{ (matrix.prepared_statements && 'w/ prepared statements') || '' }}
35
+ env:
36
+ BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
37
+ PREPARED_STATEMENTS: ${{ matrix.prepared_statements && '1' }}
38
+ steps:
39
+ - uses: actions/checkout@v2
40
+ - run: |
41
+ docker-compose up -d
42
+ - uses: ruby/setup-ruby@v1
43
+ with:
44
+ ruby-version: ${{ matrix.ruby }}
45
+ bundler-cache: true
46
+ - run: |
47
+ bundle exec rake spec
data/.gitignore CHANGED
@@ -2,3 +2,5 @@ spec/debug.log
2
2
  pkg/
3
3
  *.rb#
4
4
  *.*~
5
+ Gemfile.lock
6
+ *.gemfile.lock
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --force-color
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
@@ -18,16 +6,8 @@ appraise 'rails-6.1' do
18
6
  gem 'rails', '~> 6.1.0'
19
7
  end
20
8
 
21
- appraise 'active-record-5.2' do
22
- gem 'activerecord', '~> 5.2.0'
23
- gem 'i18n', '~> 0.9.5'
24
- gem 'nokogiri', '~> 1.7.1'
25
- gem 'nio4r', '~> 2.3.1'
26
- gem 'sprockets', '~> 3.7.1'
27
- gem 'byebug', '~> 11.0'
28
- gem 'rake', '12.0.0'
29
- gem 'redis', '3.3.3'
30
- gem 'pry-byebug', '3.9.0'
9
+ appraise 'rails-7.0' do
10
+ gem 'rails', '~> 7.0.0'
31
11
  end
32
12
 
33
13
  appraise 'active-record-6.0' do
@@ -37,3 +17,7 @@ end
37
17
  appraise 'active-record-6.1' do
38
18
  gem 'activerecord', '~> 6.1.0'
39
19
  end
20
+
21
+ appraise 'active-record-7.0' do
22
+ gem 'activerecord', '~> 7.0.0'
23
+ end
data/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
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
+
32
+ ## 2.0.0 2022-05-19
33
+
34
+ * Replace RequestStore with CurrentAttributes [#139](https://github.com/citusdata/activerecord-multi-tenant/pull/139)
35
+ * Support changing table_name after calling multi_tenant [#128](https://github.com/citusdata/activerecord-multi-tenant/pull/128)
36
+ * Allow to use uuid as primary key on partition table [#112](https://github.com/citusdata/activerecord-multi-tenant/pull/112)
37
+ * Support latest Rails 5.2 [#145](https://github.com/citusdata/activerecord-multi-tenant/pull/145)
38
+ * Support optional: true for belongs_to [#147](https://github.com/citusdata/activerecord-multi-tenant/pull/147)
39
+
40
+
41
+ ## 1.2.0 2022-03-29
42
+
43
+ * Test Rails 7 & Ruby 3
44
+ * Fix regression in 1.1.1 involving deleted tenants [#123](https://github.com/citusdata/activerecord-multi-tenant/pull/123)
45
+ * Fix incorrect SQL generated when joining two models and one has a default scope [#132](https://github.com/citusdata/activerecord-multi-tenant/pull/132)
46
+ * Update for Rails 5+ removal of type_cast_for_database [#135](https://github.com/citusdata/activerecord-multi-tenant/pull/135)
47
+
48
+
3
49
  ## 1.1.1 2021-01-15
4
50
 
5
51
  * Add support for Rails 6.1 [#108](https://github.com/citusdata/activerecord-multi-tenant/pull/108)
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,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', '>= 6'
20
19
 
21
20
  s.add_development_dependency 'rspec', '>= 3.0'
22
21
  s.add_development_dependency 'rspec-rails'
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "activerecord", "~> 7.0.0"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "rails", "~> 7.0.0"
7
+
8
+ gemspec path: "../"
@@ -9,7 +9,7 @@ module MultiTenant
9
9
  end
10
10
 
11
11
  def <<(row)
12
- row = row.map.with_index { |val, idx| @column_types[idx].type_cast_for_database(val) }
12
+ row = row.map.with_index { |val, idx| @column_types[idx].serialize(val) }
13
13
  @conn.put_copy_data(row)
14
14
  @count += 1
15
15
  end
@@ -18,7 +18,7 @@ module MultiTenant
18
18
  module CopyFromClient
19
19
  def copy_from_client(columns, &block)
20
20
  conn = connection.raw_connection
21
- column_types = columns.map { |c| columns_hash[c.to_s] }
21
+ column_types = columns.map { |c| type_for_attribute(c.to_s) }
22
22
  helper = MultiTenant::CopyFromClientHelper.new(conn, column_types)
23
23
  conn.copy_data %{COPY #{quoted_table_name}("#{columns.join('","')}") FROM STDIN}, PG::TextEncoder::CopyRow.new do
24
24
  block.call helper
@@ -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,12 +1,20 @@
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
- before_create -> { self.id ||= self.class.connection.select_value("SELECT nextval('" + [self.class.table_name, self.class.primary_key, 'seq'].join('_') + "'::regclass)") }
11
+ before_create -> do
12
+ if self.class.columns_hash[self.class.primary_key].type == :uuid
13
+ self.id ||= SecureRandom.uuid
14
+ else
15
+ self.id ||= self.class.connection.select_value("SELECT nextval('#{self.class.table_name}_#{self.class.primary_key}_seq'::regclass)")
16
+ end
17
+ end
10
18
  end
11
19
  else
12
20
  class << self
@@ -38,18 +46,18 @@ module MultiTenant
38
46
 
39
47
  def inherited(subclass)
40
48
  super
41
- MultiTenant.register_multi_tenant_model(subclass.table_name, subclass) if subclass.table_name
49
+ MultiTenant.register_multi_tenant_model(subclass)
42
50
  end
43
51
  end
44
52
 
45
- MultiTenant.register_multi_tenant_model(table_name, self) if table_name
53
+ MultiTenant.register_multi_tenant_model(self)
46
54
 
47
55
  @partition_key = options[:partition_key] || MultiTenant.partition_key(tenant_name)
48
56
  partition_key = @partition_key
49
57
 
50
58
  # Create an implicit belongs_to association only if tenant class exists
51
59
  if MultiTenant.tenant_klass_defined?(tenant_name)
52
- belongs_to tenant_name, **options.slice(:class_name, :inverse_of).merge(foreign_key: options[:partition_key])
60
+ belongs_to tenant_name, **options.slice(:class_name, :inverse_of, :optional).merge(foreign_key: options[:partition_key])
53
61
  end
54
62
 
55
63
  # New instances should have the tenant set
@@ -67,7 +75,6 @@ module MultiTenant
67
75
  # Rails 5 `attribute_will_change!` uses the attribute-method-call rather than `read_attribute`
68
76
  # and will raise ActiveModel::MissingAttributeError if that column was not selected.
69
77
  # This is rescued as NoMethodError and in MRI attribute_was is assigned an arbitrary Object
70
- # This is still true after the Rails 5.2 refactor
71
78
  was = send("#{partition_key}_was")
72
79
  was_nil_or_skipped = was.nil? || was.class == Object
73
80
 
@@ -94,7 +101,8 @@ module MultiTenant
94
101
  include to_include
95
102
 
96
103
  around_save -> (record, block) {
97
- 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?
98
106
  MultiTenant.with(record.public_send(partition_key)) { block.call }
99
107
  else
100
108
  block.call
@@ -102,7 +110,8 @@ module MultiTenant
102
110
  }
103
111
 
104
112
  around_update -> (record, block) {
105
- if MultiTenant.current_tenant_id.nil?
113
+ record_tenant = record.attribute_was(partition_key)
114
+ if MultiTenant.current_tenant_id.nil? && !record_tenant.nil?
106
115
  MultiTenant.with(record.public_send(partition_key)) { block.call }
107
116
  else
108
117
  block.call
@@ -123,6 +132,20 @@ end
123
132
 
124
133
  ActiveSupport.on_load(:active_record) do |base|
125
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
126
149
  end
127
150
 
128
151
  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,41 @@ 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
+ 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
+
98
147
  # Preserve backward compatibility for people using .with_id
99
148
  singleton_class.send(:alias_method, :with_id, :with)
100
149
 
@@ -122,6 +122,8 @@ module MultiTenant
122
122
  alias :visit_Arel_Nodes_FullOuterJoin :visit_Arel_Nodes_OuterJoin
123
123
  alias :visit_Arel_Nodes_RightOuterJoin :visit_Arel_Nodes_OuterJoin
124
124
 
125
+ alias :visit_ActiveModel_Attribute :terminal
126
+
125
127
  private
126
128
 
127
129
  def tenant_relation?(table_name)
@@ -274,7 +276,11 @@ module ActiveRecord
274
276
  if node.wheres.empty?
275
277
  node.wheres = [enforcement_clause]
276
278
  else
277
- node.wheres[0] = enforcement_clause.and(node.wheres[0])
279
+ if node.wheres[0].is_a?(Arel::Nodes::And)
280
+ node.wheres[0].children << enforcement_clause
281
+ else
282
+ node.wheres[0] = enforcement_clause.and(node.wheres[0])
283
+ end
278
284
  end
279
285
  else
280
286
  raise "UnknownContext"
@@ -289,12 +295,9 @@ module ActiveRecord
289
295
  end
290
296
 
291
297
  node_list.select{ |n| n.is_a? Arel::Nodes::Join }.each do |node_join|
292
- if (!node_join.right ||
293
- (ActiveRecord::VERSION::MAJOR == 5 &&
294
- !node_join.right.expr.right.is_a?(Arel::Attributes::Attribute)))
298
+ if !node_join.right
295
299
  next
296
300
  end
297
-
298
301
  relation_right, relation_left = relations_from_node_join(node_join)
299
302
 
300
303
  next unless relation_right && relation_left
@@ -302,7 +305,7 @@ module ActiveRecord
302
305
  model_right = MultiTenant.multi_tenant_model_for_table(relation_left.table_name)
303
306
  model_left = MultiTenant.multi_tenant_model_for_table(relation_right.table_name)
304
307
  if model_right && model_left
305
- 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)
306
309
  node_join.right.expr = node_join.right.expr.and(join_enforcement_clause)
307
310
  end
308
311
  end
@@ -316,19 +319,20 @@ module ActiveRecord
316
319
 
317
320
  private
318
321
  def relations_from_node_join(node_join)
319
- 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)
320
323
  return node_join.right.expr.right.relation, node_join.right.expr.left.relation
321
324
  end
322
325
 
323
- children = node_join.right.expr.children
326
+ children = [node_join.right.expr.children].flatten
324
327
 
325
- 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)}
326
329
  if tenant_applied || children.empty?
327
330
  return nil, nil
328
331
  end
329
332
 
330
- if children[0].right.respond_to?('relation') && children[0].left.respond_to?('relation')
331
- 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
332
336
  end
333
337
 
334
338
  return nil, nil
@@ -18,7 +18,11 @@ module Sidekiq::Middleware::MultiTenant
18
18
  class Server
19
19
  def call(worker_class, msg, queue)
20
20
  if msg.has_key?('multi_tenant')
21
- tenant = msg['multi_tenant']['class'].constantize.find(msg['multi_tenant']['id'])
21
+ tenant = begin
22
+ msg['multi_tenant']['class'].constantize.find(msg['multi_tenant']['id'])
23
+ rescue ActiveRecord::RecordNotFound
24
+ msg['multi_tenant']['id']
25
+ end
22
26
  MultiTenant.with(tenant) do
23
27
  yield
24
28
  end
@@ -29,6 +33,22 @@ module Sidekiq::Middleware::MultiTenant
29
33
  end
30
34
  end
31
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
+
32
52
  module Sidekiq
33
53
  class Client
34
54
  def push_bulk_with_tenants(items)
@@ -1,3 +1,3 @@
1
1
  module MultiTenant
2
- VERSION = '1.1.1'
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'