activerecord-multi-tenant 1.2.0 → 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 +0 -16
- data/Appraisals +0 -24
- data/CHANGELOG.md +38 -0
- data/README.md +1 -1
- data/activerecord-multi-tenant.gemspec +1 -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 +8 -10
- data/lib/activerecord-multi-tenant/sidekiq.rb +16 -0
- 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 +130 -6
- data/spec/activerecord-multi-tenant/record_modifications_spec.rb +19 -0
- data/spec/schema.rb +23 -0
- data/spec/spec_helper.rb +7 -0
- metadata +5 -21
- data/gemfiles/active_record_5.2.gemfile +0 -16
- data/gemfiles/rails_5.2.gemfile +0 -16
- 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
|
data/.github/workflows/CI.yml
CHANGED
|
@@ -13,40 +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
|
|
23
21
|
- rails_6.0
|
|
24
22
|
- rails_6.1
|
|
25
23
|
- rails_7.0
|
|
26
|
-
- active_record_5.2
|
|
27
24
|
- active_record_6.0
|
|
28
25
|
- active_record_6.1
|
|
29
26
|
- active_record_7.0
|
|
30
27
|
prepared_statements: [true, false]
|
|
31
28
|
exclude:
|
|
32
29
|
# activesupport-7.0.0 requires ruby version >= 2.7.0
|
|
33
|
-
- ruby: '2.5'
|
|
34
|
-
gemfile: 'rails_7.0'
|
|
35
|
-
- ruby: '2.5'
|
|
36
|
-
gemfile: 'active_record_7.0'
|
|
37
30
|
- ruby: '2.6'
|
|
38
31
|
gemfile: 'rails_7.0'
|
|
39
32
|
- ruby: '2.6'
|
|
40
33
|
gemfile: 'active_record_7.0'
|
|
41
|
-
# ruby >3 and activesupport 5.2 are not compatible
|
|
42
|
-
- ruby: '3.0'
|
|
43
|
-
gemfile: 'rails_5.2'
|
|
44
|
-
- ruby: '3.0'
|
|
45
|
-
gemfile: 'active_record_5.2'
|
|
46
|
-
- ruby: '3.1'
|
|
47
|
-
gemfile: 'rails_5.2'
|
|
48
|
-
- ruby: '3.1'
|
|
49
|
-
gemfile: 'active_record_5.2'
|
|
50
34
|
name: Ruby ${{ matrix.ruby }} / ${{ matrix.gemfile }} ${{ (matrix.prepared_statements && 'w/ prepared statements') || '' }}
|
|
51
35
|
env:
|
|
52
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,43 @@
|
|
|
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
|
+
|
|
3
41
|
## 1.2.0 2022-03-29
|
|
4
42
|
|
|
5
43
|
* Test Rails 7 & Ruby 3
|
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'
|
|
@@ -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
|
|
|
@@ -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
|
|
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(
|
|
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,19 +319,20 @@ module ActiveRecord
|
|
|
322
319
|
|
|
323
320
|
private
|
|
324
321
|
def relations_from_node_join(node_join)
|
|
325
|
-
if
|
|
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) ||
|
|
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
|
|
335
332
|
|
|
336
|
-
|
|
337
|
-
|
|
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
|
|
338
336
|
end
|
|
339
337
|
|
|
340
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)
|
|
@@ -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,22 @@ 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
|
+
|
|
212
|
+
it 'handles changing tenant from nil to a value' do
|
|
213
|
+
record = OptionalSubTask.create(sub_task_id: sub_task.id)
|
|
214
|
+
expect(record.reload.sub_task).to eq(sub_task)
|
|
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)
|
|
220
|
+
end
|
|
221
|
+
|
|
147
222
|
it 'handles has_many through' do
|
|
148
223
|
MultiTenant.with(account) do
|
|
149
224
|
expect(project.sub_tasks).to eq [sub_task]
|
|
@@ -234,6 +309,55 @@ describe MultiTenant do
|
|
|
234
309
|
end
|
|
235
310
|
end
|
|
236
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
|
+
|
|
237
361
|
# ::with
|
|
238
362
|
describe "::with" do
|
|
239
363
|
it "should set current_tenant to the specified tenant inside the block" do
|
|
@@ -349,10 +473,10 @@ describe MultiTenant do
|
|
|
349
473
|
|
|
350
474
|
it "applies the team_id conditions in the where clause" do
|
|
351
475
|
option1 = <<-sql.strip
|
|
352
|
-
SELECT "sub_tasks".* FROM "sub_tasks" INNER JOIN "tasks" ON "sub_tasks"."task_id" = "tasks"."id" AND "
|
|
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
|
|
353
477
|
sql
|
|
354
478
|
option2 = <<-sql.strip
|
|
355
|
-
SELECT "sub_tasks".* FROM "sub_tasks" INNER JOIN "tasks" ON "sub_tasks"."task_id" = "tasks"."id" AND "
|
|
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
|
|
356
480
|
sql
|
|
357
481
|
|
|
358
482
|
account1 = Account.create! name: 'Account 1'
|
|
@@ -367,7 +491,7 @@ describe MultiTenant do
|
|
|
367
491
|
|
|
368
492
|
MultiTenant.without do
|
|
369
493
|
expected_sql = <<-sql
|
|
370
|
-
SELECT "sub_tasks".* FROM "sub_tasks" INNER JOIN "tasks" ON "sub_tasks"."task_id" = "tasks"."id" AND "
|
|
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
|
|
371
495
|
sql
|
|
372
496
|
|
|
373
497
|
project = Project.first
|
|
@@ -405,7 +529,7 @@ describe MultiTenant do
|
|
|
405
529
|
expect(project.categories).to include(category1)
|
|
406
530
|
|
|
407
531
|
expected_sql = <<-sql
|
|
408
|
-
SELECT "projects".* FROM "projects" INNER JOIN "project_categories" ON "project_categories"."project_id" = "projects"."id" AND "
|
|
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
|
|
409
533
|
sql
|
|
410
534
|
|
|
411
535
|
expect(Project.where(account_id: 1).joins(:categories).to_sql).to eq(expected_sql.strip)
|
|
@@ -439,7 +563,7 @@ describe MultiTenant do
|
|
|
439
563
|
|
|
440
564
|
MultiTenant.without do
|
|
441
565
|
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 "
|
|
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
|
|
443
567
|
sql
|
|
444
568
|
|
|
445
569
|
expect(Project.where(account_id: 1).eager_load(:categories).to_sql).to eq(expected_sql.strip)
|
|
@@ -471,7 +595,7 @@ describe MultiTenant do
|
|
|
471
595
|
|
|
472
596
|
MultiTenant.without do
|
|
473
597
|
expected_sql = <<-sql
|
|
474
|
-
SELECT "tasks".* FROM "tasks" INNER JOIN "projects" ON "projects"."id" = "tasks"."project_id" AND "
|
|
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
|
|
475
599
|
sql
|
|
476
600
|
|
|
477
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
|
@@ -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:
|
|
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-
|
|
11
|
+
date: 2022-12-06 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: '
|
|
19
|
+
version: '6'
|
|
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: '
|
|
26
|
+
version: '6'
|
|
41
27
|
- !ruby/object:Gem::Dependency
|
|
42
28
|
name: rspec
|
|
43
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -168,11 +154,9 @@ files:
|
|
|
168
154
|
- activerecord-multi-tenant.gemspec
|
|
169
155
|
- docker-compose.yml
|
|
170
156
|
- gemfiles/.bundle/config
|
|
171
|
-
- gemfiles/active_record_5.2.gemfile
|
|
172
157
|
- gemfiles/active_record_6.0.gemfile
|
|
173
158
|
- gemfiles/active_record_6.1.gemfile
|
|
174
159
|
- gemfiles/active_record_7.0.gemfile
|
|
175
|
-
- gemfiles/rails_5.2.gemfile
|
|
176
160
|
- gemfiles/rails_6.0.gemfile
|
|
177
161
|
- gemfiles/rails_6.1.gemfile
|
|
178
162
|
- gemfiles/rails_7.0.gemfile
|
|
@@ -184,12 +168,12 @@ files:
|
|
|
184
168
|
- lib/activerecord-multi-tenant/migrations.rb
|
|
185
169
|
- lib/activerecord-multi-tenant/model_extensions.rb
|
|
186
170
|
- lib/activerecord-multi-tenant/multi_tenant.rb
|
|
187
|
-
- lib/activerecord-multi-tenant/persistence_extension.rb
|
|
188
171
|
- lib/activerecord-multi-tenant/query_monitor.rb
|
|
189
172
|
- lib/activerecord-multi-tenant/query_rewriter.rb
|
|
190
173
|
- lib/activerecord-multi-tenant/sidekiq.rb
|
|
191
174
|
- lib/activerecord-multi-tenant/version.rb
|
|
192
175
|
- lib/activerecord-multi-tenant/with_lock.rb
|
|
176
|
+
- spec/activerecord-multi-tenant/associations_spec.rb
|
|
193
177
|
- spec/activerecord-multi-tenant/controller_extensions_spec.rb
|
|
194
178
|
- spec/activerecord-multi-tenant/fast_truncate_spec.rb
|
|
195
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: "../"
|
data/gemfiles/rails_5.2.gemfile
DELETED
|
@@ -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,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
|