activerecord-multi-tenant 1.1.1 → 2.2.0
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 +4 -4
- data/.github/workflows/CI.yml +47 -0
- data/.gitignore +2 -0
- data/.rspec +1 -0
- data/Appraisals +6 -22
- data/CHANGELOG.md +46 -0
- data/README.md +1 -1
- data/activerecord-multi-tenant.gemspec +1 -2
- data/gemfiles/active_record_7.0.gemfile +8 -0
- data/gemfiles/rails_7.0.gemfile +8 -0
- data/lib/activerecord-multi-tenant/copy_from_client.rb +2 -2
- data/lib/activerecord-multi-tenant/migrations.rb +76 -9
- data/lib/activerecord-multi-tenant/model_extensions.rb +31 -8
- data/lib/activerecord-multi-tenant/multi_tenant.rb +57 -8
- data/lib/activerecord-multi-tenant/query_rewriter.rb +15 -11
- data/lib/activerecord-multi-tenant/sidekiq.rb +21 -1
- data/lib/activerecord-multi-tenant/version.rb +1 -1
- data/lib/activerecord-multi-tenant.rb +0 -1
- data/spec/activerecord-multi-tenant/associations_spec.rb +21 -0
- data/spec/activerecord-multi-tenant/model_extensions_spec.rb +186 -54
- data/spec/activerecord-multi-tenant/query_rewriter_spec.rb +17 -0
- data/spec/activerecord-multi-tenant/record_modifications_spec.rb +19 -0
- data/spec/activerecord-multi-tenant/sidekiq_spec.rb +11 -0
- data/spec/schema.rb +50 -4
- data/spec/spec_helper.rb +7 -0
- metadata +13 -33
- data/.travis.yml +0 -34
- data/Gemfile.lock +0 -199
- data/gemfiles/active_record_5.2.gemfile +0 -16
- data/gemfiles/active_record_5.2.gemfile.lock +0 -188
- data/gemfiles/active_record_6.0.gemfile.lock +0 -198
- data/gemfiles/active_record_6.1.gemfile.lock +0 -198
- data/gemfiles/rails_5.2.gemfile +0 -16
- data/gemfiles/rails_5.2.gemfile.lock +0 -188
- data/gemfiles/rails_6.0.gemfile.lock +0 -198
- data/gemfiles/rails_6.1.gemfile.lock +0 -198
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 15ad19bf20781129dc1bd57d4e4b8eb8731044cea3b89f29a62789d0f5a45aee
|
4
|
+
data.tar.gz: 0f76290b00a7d540495972ab5a9de09d3abbb1dde11758d58b08493bf743b5da
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
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 '
|
22
|
-
gem '
|
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
|
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.
|
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'
|
@@ -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].
|
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|
|
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
|
-
|
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
|
-
|
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 ==
|
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
|
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 ->
|
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
|
49
|
+
MultiTenant.register_multi_tenant_model(subclass)
|
42
50
|
end
|
43
51
|
end
|
44
52
|
|
45
|
-
MultiTenant.register_multi_tenant_model(
|
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
|
-
|
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
|
-
|
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 '
|
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(
|
28
|
-
@@multi_tenant_models ||=
|
29
|
-
@@multi_tenant_models
|
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
|
-
|
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
|
-
|
60
|
+
Current.tenant = tenant
|
47
61
|
end
|
48
62
|
|
49
63
|
def self.current_tenant
|
50
|
-
|
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]
|
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
|
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(
|
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
|
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) ||
|
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
|
-
|
331
|
-
|
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 =
|
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)
|
@@ -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'
|