activerecord-multi-tenant 1.2.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/CI.yml +10 -0
- data/CHANGELOG.md +17 -0
- data/activerecord-multi-tenant.gemspec +1 -2
- data/gemfiles/active_record_5.2.3.gemfile +16 -0
- data/gemfiles/active_record_5.2.gemfile +1 -1
- data/gemfiles/rails_5.2.3.gemfile +16 -0
- data/gemfiles/rails_5.2.gemfile +1 -1
- data/lib/activerecord-multi-tenant/migrations.rb +69 -8
- data/lib/activerecord-multi-tenant/model_extensions.rb +25 -5
- data/lib/activerecord-multi-tenant/multi_tenant.rb +39 -8
- data/lib/activerecord-multi-tenant/query_rewriter.rb +5 -8
- 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 +71 -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 +7 -19
- 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: e198970d8d5a4db04c8e203e28021668c765fdde78f9bb9a7cd0e6c577bfbe0e
|
|
4
|
+
data.tar.gz: 6abb30aac520063dbef7ddc5689714ef1dd9eb691aa382498b30c8bbcf03844b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5010dbbd2a8036ad5a3adae79a49567a279a318931ffe832663dd2430f455d1895e26cca6d96456484b894cc241cca21fbab31aa301f9ab4ef0538a4266b4005
|
|
7
|
+
data.tar.gz: e94b7ee0f619b0763879efefbae3a69792e00e8b9f2a2661a82666d4c129decea132f20b44999177bdc0162dec0e9b3cac4f402d70a76fb14518e283f4441d6c
|
data/.github/workflows/CI.yml
CHANGED
|
@@ -19,10 +19,12 @@ jobs:
|
|
|
19
19
|
- '3.0'
|
|
20
20
|
- '3.1'
|
|
21
21
|
gemfile:
|
|
22
|
+
- rails_5.2.3
|
|
22
23
|
- rails_5.2
|
|
23
24
|
- rails_6.0
|
|
24
25
|
- rails_6.1
|
|
25
26
|
- rails_7.0
|
|
27
|
+
- active_record_5.2.3
|
|
26
28
|
- active_record_5.2
|
|
27
29
|
- active_record_6.0
|
|
28
30
|
- active_record_6.1
|
|
@@ -47,6 +49,14 @@ jobs:
|
|
|
47
49
|
gemfile: 'rails_5.2'
|
|
48
50
|
- ruby: '3.1'
|
|
49
51
|
gemfile: 'active_record_5.2'
|
|
52
|
+
- ruby: '3.0'
|
|
53
|
+
gemfile: 'rails_5.2.3'
|
|
54
|
+
- ruby: '3.0'
|
|
55
|
+
gemfile: 'active_record_5.2.3'
|
|
56
|
+
- ruby: '3.1'
|
|
57
|
+
gemfile: 'rails_5.2.3'
|
|
58
|
+
- ruby: '3.1'
|
|
59
|
+
gemfile: 'active_record_5.2.3'
|
|
50
60
|
name: Ruby ${{ matrix.ruby }} / ${{ matrix.gemfile }} ${{ (matrix.prepared_statements && 'w/ prepared statements') || '' }}
|
|
51
61
|
env:
|
|
52
62
|
BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.1.0 2022-10-20
|
|
4
|
+
* Fix query building for models with mismatched partition_keys [#150](https://github.com/citusdata/activerecord-multi-tenant/pull/150)
|
|
5
|
+
* Identify tenant even if class name is nonstandard [#152](https://github.com/citusdata/activerecord-multi-tenant/pull/152)
|
|
6
|
+
* Add current_tenant_id to WHERE clauses when calling methods on activerecord instance or its associations [#154](https://github.com/citusdata/activerecord-multi-tenant/pull/154)
|
|
7
|
+
* Make create_distributed_table, create_reference_table reversible & add ruby wrapper for rebalance_table_shards [#155](https://github.com/citusdata/activerecord-multi-tenant/pull/155)
|
|
8
|
+
* Support create_distributed_table, create_reference_table in schema.rb [#156](https://github.com/citusdata/activerecord-multi-tenant/pull/156)
|
|
9
|
+
* Add client and server sidekiq middleware to sidekiq middleware chain [#158](https://github.com/citusdata/activerecord-multi-tenant/pull/158)
|
|
10
|
+
|
|
11
|
+
## 2.0.0 2022-05-19
|
|
12
|
+
|
|
13
|
+
* Replace RequestStore with CurrentAttributes [#139](https://github.com/citusdata/activerecord-multi-tenant/pull/139)
|
|
14
|
+
* Support changing table_name after calling multi_tenant [#128](https://github.com/citusdata/activerecord-multi-tenant/pull/128)
|
|
15
|
+
* Allow to use uuid as primary key on partition table [#112](https://github.com/citusdata/activerecord-multi-tenant/pull/112)
|
|
16
|
+
* Support latest Rails 5.2 [#145](https://github.com/citusdata/activerecord-multi-tenant/pull/145)
|
|
17
|
+
* Support optional: true for belongs_to [#147](https://github.com/citusdata/activerecord-multi-tenant/pull/147)
|
|
18
|
+
|
|
19
|
+
|
|
3
20
|
## 1.2.0 2022-03-29
|
|
4
21
|
|
|
5
22
|
* Test Rails 7 & Ruby 3
|
|
@@ -15,8 +15,7 @@ Gem::Specification.new do |s|
|
|
|
15
15
|
s.homepage = 'https://github.com/citusdata/activerecord-multi-tenant'
|
|
16
16
|
s.license = 'MIT'
|
|
17
17
|
|
|
18
|
-
s.
|
|
19
|
-
s.add_dependency('rails','>= 4.2')
|
|
18
|
+
s.add_dependency 'rails', '>= 5.2'
|
|
20
19
|
|
|
21
20
|
s.add_development_dependency 'rspec', '>= 3.0'
|
|
22
21
|
s.add_development_dependency 'rspec-rails'
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "appraisal"
|
|
6
|
+
gem "activerecord", "~> 5.2.0", "< 5.2.4" # FIXME
|
|
7
|
+
gem "i18n", "~> 0.9.5"
|
|
8
|
+
gem "nokogiri", "~> 1.7.1"
|
|
9
|
+
gem "nio4r", "~> 2.3.1"
|
|
10
|
+
gem "sprockets", "~> 3.7.1"
|
|
11
|
+
gem "byebug", "~> 11.0"
|
|
12
|
+
gem "rake", "12.0.0"
|
|
13
|
+
gem "redis", "3.3.3"
|
|
14
|
+
gem "pry-byebug", "3.9.0"
|
|
15
|
+
|
|
16
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "appraisal"
|
|
6
|
+
gem "rails", "~> 5.2.0", "< 5.2.4" # FIXME
|
|
7
|
+
gem "i18n", "~> 0.9.5"
|
|
8
|
+
gem "nokogiri", "~> 1.7.1"
|
|
9
|
+
gem "nio4r", "~> 2.3.1"
|
|
10
|
+
gem "sprockets", "~> 3.7.1"
|
|
11
|
+
gem "byebug", "~> 11.0"
|
|
12
|
+
gem "rake", "12.0.0"
|
|
13
|
+
gem "redis", "3.3.3"
|
|
14
|
+
gem "pry-byebug", "3.9.0"
|
|
15
|
+
|
|
16
|
+
gemspec path: "../"
|
data/gemfiles/rails_5.2.gemfile
CHANGED
|
@@ -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,21 +56,19 @@ module MultiTenant
|
|
|
28
56
|
end
|
|
29
57
|
|
|
30
58
|
def citus_version
|
|
31
|
-
execute("SELECT extversion FROM pg_extension WHERE extname = 'citus'").getvalue(0,0).try(:split, '-').try(:first)
|
|
59
|
+
execute("SELECT extversion FROM pg_extension WHERE extname = 'citus'").getvalue(0, 0).try(:split, '-').try(:first)
|
|
32
60
|
rescue ArgumentError => e
|
|
33
|
-
raise unless e.message ==
|
|
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
74
|
if options[:partition_key] && options[:partition_key].to_s != 'id'
|
|
@@ -54,3 +80,38 @@ module ActiveRecord
|
|
|
54
80
|
end
|
|
55
81
|
end
|
|
56
82
|
end
|
|
83
|
+
|
|
84
|
+
module ActiveRecord
|
|
85
|
+
class SchemaDumper
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
alias initialize_without_citus initialize
|
|
89
|
+
def initialize(connection, options = {})
|
|
90
|
+
initialize_without_citus(connection, options)
|
|
91
|
+
|
|
92
|
+
@distribution_columns =
|
|
93
|
+
if ActiveRecord::Migration.citus_version.present?
|
|
94
|
+
@connection.execute('SELECT logicalrelid::regclass AS table_name, column_to_column_name(logicalrelid, partkey) AS dist_col_name FROM pg_dist_partition').to_h do |v|
|
|
95
|
+
[v['table_name'], v['dist_col_name']]
|
|
96
|
+
end
|
|
97
|
+
else
|
|
98
|
+
{}
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Support for create_distributed_table & create_reference_table
|
|
103
|
+
alias table_without_citus table
|
|
104
|
+
def table(table, stream)
|
|
105
|
+
table_without_citus(table, stream)
|
|
106
|
+
table_name = remove_prefix_and_suffix(table)
|
|
107
|
+
distribution_column = @distribution_columns[table_name]
|
|
108
|
+
if distribution_column
|
|
109
|
+
stream.puts " create_distributed_table(#{table_name.inspect}, #{distribution_column.inspect})"
|
|
110
|
+
stream.puts
|
|
111
|
+
elsif @distribution_columns.key?(table_name)
|
|
112
|
+
stream.puts " create_reference_table(#{table_name.inspect})"
|
|
113
|
+
stream.puts
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -3,10 +3,16 @@ module MultiTenant
|
|
|
3
3
|
DEFAULT_ID_FIELD = 'id'.freeze
|
|
4
4
|
|
|
5
5
|
def multi_tenant(tenant_name, options = {})
|
|
6
|
-
if to_s.underscore.to_sym == tenant_name
|
|
6
|
+
if to_s.underscore.to_sym == tenant_name || (!table_name.nil? && table_name.singularize.to_sym == tenant_name)
|
|
7
7
|
unless MultiTenant.with_write_only_mode_enabled?
|
|
8
8
|
# This is the tenant model itself. Workaround for https://github.com/citusdata/citus/issues/687
|
|
9
|
-
before_create ->
|
|
9
|
+
before_create -> do
|
|
10
|
+
if self.class.columns_hash[self.class.primary_key].type == :uuid
|
|
11
|
+
self.id ||= SecureRandom.uuid
|
|
12
|
+
else
|
|
13
|
+
self.id ||= self.class.connection.select_value("SELECT nextval('#{self.class.table_name}_#{self.class.primary_key}_seq'::regclass)")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
10
16
|
end
|
|
11
17
|
else
|
|
12
18
|
class << self
|
|
@@ -38,18 +44,18 @@ module MultiTenant
|
|
|
38
44
|
|
|
39
45
|
def inherited(subclass)
|
|
40
46
|
super
|
|
41
|
-
MultiTenant.register_multi_tenant_model(subclass
|
|
47
|
+
MultiTenant.register_multi_tenant_model(subclass)
|
|
42
48
|
end
|
|
43
49
|
end
|
|
44
50
|
|
|
45
|
-
MultiTenant.register_multi_tenant_model(
|
|
51
|
+
MultiTenant.register_multi_tenant_model(self)
|
|
46
52
|
|
|
47
53
|
@partition_key = options[:partition_key] || MultiTenant.partition_key(tenant_name)
|
|
48
54
|
partition_key = @partition_key
|
|
49
55
|
|
|
50
56
|
# Create an implicit belongs_to association only if tenant class exists
|
|
51
57
|
if MultiTenant.tenant_klass_defined?(tenant_name)
|
|
52
|
-
belongs_to tenant_name, **options.slice(:class_name, :inverse_of).merge(foreign_key: options[:partition_key])
|
|
58
|
+
belongs_to tenant_name, **options.slice(:class_name, :inverse_of, :optional).merge(foreign_key: options[:partition_key])
|
|
53
59
|
end
|
|
54
60
|
|
|
55
61
|
# New instances should have the tenant set
|
|
@@ -123,6 +129,20 @@ end
|
|
|
123
129
|
|
|
124
130
|
ActiveSupport.on_load(:active_record) do |base|
|
|
125
131
|
base.extend MultiTenant::ModelExtensionsClassMethods
|
|
132
|
+
|
|
133
|
+
# Ensure we have current_tenant_id in where clause when a cached ActiveRecord instance is being reloaded, or update_columns without callbacks is called
|
|
134
|
+
MultiTenant.wrap_methods(ActiveRecord::Base, 'self', :delete, :reload, :update_columns)
|
|
135
|
+
|
|
136
|
+
# Any queuries fired for fetching a singular association have the correct current_tenant_id in WHERE clause
|
|
137
|
+
# reload is called anytime any record's association is accessed
|
|
138
|
+
MultiTenant.wrap_methods(ActiveRecord::Associations::Association, 'owner', :reload)
|
|
139
|
+
|
|
140
|
+
# For collection associations, we need to wrap multiple methods in returned proxy so that any queries have the correct current_tenant_id in WHERE clause
|
|
141
|
+
ActiveRecord::Associations::CollectionProxy.alias_method :equals_mt, :== # Hack to prevent syntax error due to invalid method name
|
|
142
|
+
ActiveRecord::Associations::CollectionProxy.alias_method :append_mt, :<< # Hack to prevent syntax error due to invalid method name
|
|
143
|
+
MultiTenant.wrap_methods(ActiveRecord::Associations::CollectionProxy, '@association.owner', :find, :last, :take, :build, :create, :create!, :replace, :delete_all, :destroy_all, :delete, :destroy, :calculate, :pluck, :size, :empty?, :include?, :equals_mt, :records, :append_mt, :find_nth_with_limit, :find_nth_from_last, :null_scope?, :find_from_target?, :exec_queries)
|
|
144
|
+
ActiveRecord::Associations::CollectionProxy.alias_method :==, :equals_mt
|
|
145
|
+
ActiveRecord::Associations::CollectionProxy.alias_method :<<, :append_mt
|
|
126
146
|
end
|
|
127
147
|
|
|
128
148
|
class ActiveRecord::Associations::Association
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
require '
|
|
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,23 @@ module MultiTenant
|
|
|
95
109
|
end
|
|
96
110
|
end
|
|
97
111
|
|
|
112
|
+
# Wrap calls to any of `method_names` on an instance Class `klass` with MultiTenant.with when `'owner'` (evaluated in context of the klass instance) is a ActiveRecord model instance that is multi-tenant
|
|
113
|
+
def self.wrap_methods(klass, owner, *method_names)
|
|
114
|
+
method_names.each do |method_name|
|
|
115
|
+
original_method_name = :"_mt_original_#{method_name}"
|
|
116
|
+
klass.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
|
117
|
+
alias_method :#{original_method_name}, :#{method_name}
|
|
118
|
+
def #{method_name}(*args, &block)
|
|
119
|
+
if MultiTenant.multi_tenant_model_for_table(#{owner}.class.table_name).present? && #{owner}.persisted? && MultiTenant.current_tenant_id.nil?
|
|
120
|
+
MultiTenant.with(#{owner}.public_send(#{owner}.class.partition_key)) { #{original_method_name}(*args) }
|
|
121
|
+
else
|
|
122
|
+
#{original_method_name}(*args)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
CODE
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
98
129
|
# Preserve backward compatibility for people using .with_id
|
|
99
130
|
singleton_class.send(:alias_method, :with_id, :with)
|
|
100
131
|
|
|
@@ -295,12 +295,9 @@ module ActiveRecord
|
|
|
295
295
|
end
|
|
296
296
|
|
|
297
297
|
node_list.select{ |n| n.is_a? Arel::Nodes::Join }.each do |node_join|
|
|
298
|
-
if
|
|
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,13 +319,13 @@ 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
|
|
@@ -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,12 @@ describe MultiTenant do
|
|
|
144
203
|
end
|
|
145
204
|
end
|
|
146
205
|
|
|
206
|
+
it 'handles belongs_to with optional: true' do
|
|
207
|
+
record = OptionalSubTask.create(sub_task_id: sub_task.id)
|
|
208
|
+
expect(record.reload.sub_task).to eq(sub_task)
|
|
209
|
+
expect(record.account_id).to eq(nil)
|
|
210
|
+
end
|
|
211
|
+
|
|
147
212
|
it 'handles has_many through' do
|
|
148
213
|
MultiTenant.with(account) do
|
|
149
214
|
expect(project.sub_tasks).to eq [sub_task]
|
|
@@ -349,10 +414,10 @@ describe MultiTenant do
|
|
|
349
414
|
|
|
350
415
|
it "applies the team_id conditions in the where clause" do
|
|
351
416
|
option1 = <<-sql.strip
|
|
352
|
-
SELECT "sub_tasks".* FROM "sub_tasks" INNER JOIN "tasks" ON "sub_tasks"."task_id" = "tasks"."id" AND "
|
|
417
|
+
SELECT "sub_tasks".* FROM "sub_tasks" INNER JOIN "tasks" ON "sub_tasks"."task_id" = "tasks"."id" AND "tasks"."account_id" = "sub_tasks"."account_id" WHERE "tasks"."project_id" = 1 AND "sub_tasks"."account_id" = 1 AND "tasks"."account_id" = 1
|
|
353
418
|
sql
|
|
354
419
|
option2 = <<-sql.strip
|
|
355
|
-
SELECT "sub_tasks".* FROM "sub_tasks" INNER JOIN "tasks" ON "sub_tasks"."task_id" = "tasks"."id" AND "
|
|
420
|
+
SELECT "sub_tasks".* FROM "sub_tasks" INNER JOIN "tasks" ON "sub_tasks"."task_id" = "tasks"."id" AND "tasks"."account_id" = "sub_tasks"."account_id" WHERE "sub_tasks"."account_id" = 1 AND "tasks"."project_id" = 1 AND "tasks"."account_id" = 1
|
|
356
421
|
sql
|
|
357
422
|
|
|
358
423
|
account1 = Account.create! name: 'Account 1'
|
|
@@ -367,7 +432,7 @@ describe MultiTenant do
|
|
|
367
432
|
|
|
368
433
|
MultiTenant.without do
|
|
369
434
|
expected_sql = <<-sql
|
|
370
|
-
SELECT "sub_tasks".* FROM "sub_tasks" INNER JOIN "tasks" ON "sub_tasks"."task_id" = "tasks"."id" AND "
|
|
435
|
+
SELECT "sub_tasks".* FROM "sub_tasks" INNER JOIN "tasks" ON "sub_tasks"."task_id" = "tasks"."id" AND "tasks"."account_id" = "sub_tasks"."account_id" WHERE "tasks"."project_id" = 1
|
|
371
436
|
sql
|
|
372
437
|
|
|
373
438
|
project = Project.first
|
|
@@ -405,7 +470,7 @@ describe MultiTenant do
|
|
|
405
470
|
expect(project.categories).to include(category1)
|
|
406
471
|
|
|
407
472
|
expected_sql = <<-sql
|
|
408
|
-
SELECT "projects".* FROM "projects" INNER JOIN "project_categories" ON "project_categories"."project_id" = "projects"."id" AND "
|
|
473
|
+
SELECT "projects".* FROM "projects" INNER JOIN "project_categories" ON "project_categories"."project_id" = "projects"."id" AND "projects"."account_id" = "project_categories"."account_id" INNER JOIN "categories" ON "categories"."id" = "project_categories"."category_id" WHERE "projects"."account_id" = 1
|
|
409
474
|
sql
|
|
410
475
|
|
|
411
476
|
expect(Project.where(account_id: 1).joins(:categories).to_sql).to eq(expected_sql.strip)
|
|
@@ -439,7 +504,7 @@ describe MultiTenant do
|
|
|
439
504
|
|
|
440
505
|
MultiTenant.without do
|
|
441
506
|
expected_sql = <<-sql
|
|
442
|
-
SELECT "projects"."id" AS t0_r0, "projects"."account_id" AS t0_r1, "projects"."name" AS t0_r2, "categories"."id" AS t1_r0, "categories"."name" AS t1_r1 FROM "projects" LEFT OUTER JOIN "project_categories" ON "project_categories"."project_id" = "projects"."id" AND "
|
|
507
|
+
SELECT "projects"."id" AS t0_r0, "projects"."account_id" AS t0_r1, "projects"."name" AS t0_r2, "categories"."id" AS t1_r0, "categories"."name" AS t1_r1 FROM "projects" LEFT OUTER JOIN "project_categories" ON "project_categories"."project_id" = "projects"."id" AND "projects"."account_id" = "project_categories"."account_id" LEFT OUTER JOIN "categories" ON "categories"."id" = "project_categories"."category_id" WHERE "projects"."account_id" = 1
|
|
443
508
|
sql
|
|
444
509
|
|
|
445
510
|
expect(Project.where(account_id: 1).eager_load(:categories).to_sql).to eq(expected_sql.strip)
|
|
@@ -471,7 +536,7 @@ describe MultiTenant do
|
|
|
471
536
|
|
|
472
537
|
MultiTenant.without do
|
|
473
538
|
expected_sql = <<-sql
|
|
474
|
-
SELECT "tasks".* FROM "tasks" INNER JOIN "projects" ON "projects"."id" = "tasks"."project_id" AND "
|
|
539
|
+
SELECT "tasks".* FROM "tasks" INNER JOIN "projects" ON "projects"."id" = "tasks"."project_id" AND "tasks"."account_id" = "projects"."account_id" LEFT JOIN project_categories pc ON project.category_id = pc.id WHERE "tasks"."account_id" = 1
|
|
475
540
|
sql
|
|
476
541
|
|
|
477
542
|
expect(Task.where(account_id: 1).joins(:project).joins('LEFT JOIN project_categories pc ON project.category_id = pc.id').to_sql).to eq(expected_sql.strip)
|
|
@@ -54,6 +54,25 @@ describe MultiTenant, 'Record modifications' do
|
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
it 'should not update other objects with same id when calling object.update_columns' do
|
|
58
|
+
# When two records with same id but different account_id are updated, it should only update the current one
|
|
59
|
+
expect(project.account).to eq(account)
|
|
60
|
+
expect(project2.account).to eq(account2)
|
|
61
|
+
expect(project.id).to eq(project2.id)
|
|
62
|
+
|
|
63
|
+
MultiTenant.without do
|
|
64
|
+
project2.update_columns(name: 'newthing2')
|
|
65
|
+
expect(project.reload.name).to eq('something')
|
|
66
|
+
expect(project2.reload.name).to eq('newthing2')
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'should return the same object when calling object.reload' do
|
|
71
|
+
# When two records with same id but different account_id are updated, it should not return the other object
|
|
72
|
+
expect(project.reload.account_id).to eq(account.id)
|
|
73
|
+
expect(project2.reload.account_id).to eq(account2.id)
|
|
74
|
+
end
|
|
75
|
+
|
|
57
76
|
it 'test delete for reference tables' do
|
|
58
77
|
category1 = Category.create! name: 'Category 1'
|
|
59
78
|
expect(Category.count).to eq(1)
|
data/spec/schema.rb
CHANGED
|
@@ -34,6 +34,13 @@ ARGV.grep(/\w+_spec\.rb/).empty? && ActiveRecord::Schema.define(version: 1) do
|
|
|
34
34
|
t.column :type, :string
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
create_table :optional_sub_tasks, force: true do |t|
|
|
38
|
+
t.references :account, :integer
|
|
39
|
+
t.column :sub_task_id, :integer
|
|
40
|
+
t.column :name, :string
|
|
41
|
+
t.column :type, :string
|
|
42
|
+
end
|
|
43
|
+
|
|
37
44
|
create_table :countries, force: true do |t|
|
|
38
45
|
t.column :name, :string
|
|
39
46
|
end
|
|
@@ -106,6 +113,11 @@ ARGV.grep(/\w+_spec\.rb/).empty? && ActiveRecord::Schema.define(version: 1) do
|
|
|
106
113
|
t.column :domain_id, :integer
|
|
107
114
|
end
|
|
108
115
|
|
|
116
|
+
create_table :posts, force: true, partition_key: :account_id do |t|
|
|
117
|
+
t.column :account_id, :integer
|
|
118
|
+
t.column :name, :string
|
|
119
|
+
end
|
|
120
|
+
|
|
109
121
|
create_distributed_table :accounts, :id
|
|
110
122
|
create_distributed_table :projects, :account_id
|
|
111
123
|
create_distributed_table :managers, :account_id
|
|
@@ -121,6 +133,7 @@ ARGV.grep(/\w+_spec\.rb/).empty? && ActiveRecord::Schema.define(version: 1) do
|
|
|
121
133
|
create_distributed_table :allowed_places, :account_id
|
|
122
134
|
create_distributed_table :domains, :account_id
|
|
123
135
|
create_distributed_table :pages, :account_id
|
|
136
|
+
create_distributed_table :posts, :account_id
|
|
124
137
|
create_reference_table :categories
|
|
125
138
|
end
|
|
126
139
|
|
|
@@ -128,6 +141,7 @@ class Account < ActiveRecord::Base
|
|
|
128
141
|
multi_tenant :account
|
|
129
142
|
has_many :projects
|
|
130
143
|
has_one :manager, inverse_of: :account
|
|
144
|
+
has_many :optional_sub_tasks
|
|
131
145
|
end
|
|
132
146
|
|
|
133
147
|
class Project < ActiveRecord::Base
|
|
@@ -159,6 +173,14 @@ class SubTask < ActiveRecord::Base
|
|
|
159
173
|
multi_tenant :account
|
|
160
174
|
belongs_to :task
|
|
161
175
|
has_one :project, through: :task
|
|
176
|
+
has_many :optional_sub_tasks
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
with_belongs_to_required_by_default do
|
|
180
|
+
class OptionalSubTask < ActiveRecord::Base
|
|
181
|
+
multi_tenant :account, optional: true
|
|
182
|
+
belongs_to :sub_task
|
|
183
|
+
end
|
|
162
184
|
end
|
|
163
185
|
|
|
164
186
|
class StiSubTask < SubTask
|
|
@@ -198,6 +220,7 @@ class Comment < ActiveRecord::Base
|
|
|
198
220
|
end
|
|
199
221
|
|
|
200
222
|
class Organization < ActiveRecord::Base
|
|
223
|
+
multi_tenant :organization
|
|
201
224
|
has_many :uuid_records
|
|
202
225
|
end
|
|
203
226
|
|
data/spec/spec_helper.rb
CHANGED
|
@@ -46,4 +46,11 @@ def uses_prepared_statements?
|
|
|
46
46
|
ActiveRecord::Base.connection.prepared_statements
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
def with_belongs_to_required_by_default(&block)
|
|
50
|
+
default_value = ActiveRecord::Base.belongs_to_required_by_default
|
|
51
|
+
ActiveRecord::Base.belongs_to_required_by_default = true
|
|
52
|
+
yield
|
|
53
|
+
ensure
|
|
54
|
+
ActiveRecord::Base.belongs_to_required_by_default = default_value
|
|
55
|
+
end
|
|
49
56
|
require 'schema'
|
metadata
CHANGED
|
@@ -1,43 +1,29 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activerecord-multi-tenant
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 2.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Citus Data
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2022-
|
|
11
|
+
date: 2022-10-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
-
- !ruby/object:Gem::Dependency
|
|
14
|
-
name: request_store
|
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
|
16
|
-
requirements:
|
|
17
|
-
- - ">="
|
|
18
|
-
- !ruby/object:Gem::Version
|
|
19
|
-
version: 1.0.5
|
|
20
|
-
type: :runtime
|
|
21
|
-
prerelease: false
|
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
-
requirements:
|
|
24
|
-
- - ">="
|
|
25
|
-
- !ruby/object:Gem::Version
|
|
26
|
-
version: 1.0.5
|
|
27
13
|
- !ruby/object:Gem::Dependency
|
|
28
14
|
name: rails
|
|
29
15
|
requirement: !ruby/object:Gem::Requirement
|
|
30
16
|
requirements:
|
|
31
17
|
- - ">="
|
|
32
18
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '
|
|
19
|
+
version: '5.2'
|
|
34
20
|
type: :runtime
|
|
35
21
|
prerelease: false
|
|
36
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
23
|
requirements:
|
|
38
24
|
- - ">="
|
|
39
25
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '
|
|
26
|
+
version: '5.2'
|
|
41
27
|
- !ruby/object:Gem::Dependency
|
|
42
28
|
name: rspec
|
|
43
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -168,10 +154,12 @@ files:
|
|
|
168
154
|
- activerecord-multi-tenant.gemspec
|
|
169
155
|
- docker-compose.yml
|
|
170
156
|
- gemfiles/.bundle/config
|
|
157
|
+
- gemfiles/active_record_5.2.3.gemfile
|
|
171
158
|
- gemfiles/active_record_5.2.gemfile
|
|
172
159
|
- gemfiles/active_record_6.0.gemfile
|
|
173
160
|
- gemfiles/active_record_6.1.gemfile
|
|
174
161
|
- gemfiles/active_record_7.0.gemfile
|
|
162
|
+
- gemfiles/rails_5.2.3.gemfile
|
|
175
163
|
- gemfiles/rails_5.2.gemfile
|
|
176
164
|
- gemfiles/rails_6.0.gemfile
|
|
177
165
|
- gemfiles/rails_6.1.gemfile
|
|
@@ -184,12 +172,12 @@ files:
|
|
|
184
172
|
- lib/activerecord-multi-tenant/migrations.rb
|
|
185
173
|
- lib/activerecord-multi-tenant/model_extensions.rb
|
|
186
174
|
- lib/activerecord-multi-tenant/multi_tenant.rb
|
|
187
|
-
- lib/activerecord-multi-tenant/persistence_extension.rb
|
|
188
175
|
- lib/activerecord-multi-tenant/query_monitor.rb
|
|
189
176
|
- lib/activerecord-multi-tenant/query_rewriter.rb
|
|
190
177
|
- lib/activerecord-multi-tenant/sidekiq.rb
|
|
191
178
|
- lib/activerecord-multi-tenant/version.rb
|
|
192
179
|
- lib/activerecord-multi-tenant/with_lock.rb
|
|
180
|
+
- spec/activerecord-multi-tenant/associations_spec.rb
|
|
193
181
|
- spec/activerecord-multi-tenant/controller_extensions_spec.rb
|
|
194
182
|
- spec/activerecord-multi-tenant/fast_truncate_spec.rb
|
|
195
183
|
- spec/activerecord-multi-tenant/model_extensions_spec.rb
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
module ActiveRecord
|
|
2
|
-
module Persistence
|
|
3
|
-
alias :delete_orig :delete
|
|
4
|
-
|
|
5
|
-
def delete
|
|
6
|
-
if MultiTenant.multi_tenant_model_for_table(self.class.table_name).present? && persisted? && MultiTenant.current_tenant_id.nil?
|
|
7
|
-
MultiTenant.with(self.public_send(self.class.partition_key)) { delete_orig }
|
|
8
|
-
else
|
|
9
|
-
delete_orig
|
|
10
|
-
end
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|