From fb01ff76b315a0f59d2cdb3987b1ef24b6d10304 Mon Sep 17 00:00:00 2001 From: wata727 Date: Wed, 13 Oct 2021 18:22:39 +0900 Subject: [PATCH 001/105] sidekiq: Allow dequeue even if tenant not found --- lib/activerecord-multi-tenant/sidekiq.rb | 6 +++++- spec/activerecord-multi-tenant/sidekiq_spec.rb | 11 +++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/activerecord-multi-tenant/sidekiq.rb b/lib/activerecord-multi-tenant/sidekiq.rb index 2e39c9d5..4be568ad 100644 --- a/lib/activerecord-multi-tenant/sidekiq.rb +++ b/lib/activerecord-multi-tenant/sidekiq.rb @@ -18,7 +18,11 @@ def call(worker_class, msg, queue, redis_pool) class Server def call(worker_class, msg, queue) if msg.has_key?('multi_tenant') - tenant = msg['multi_tenant']['class'].constantize.find(msg['multi_tenant']['id']) + tenant = begin + msg['multi_tenant']['class'].constantize.find(msg['multi_tenant']['id']) + rescue ActiveRecord::RecordNotFound + msg['multi_tenant']['id'] + end MultiTenant.with(tenant) do yield end diff --git a/spec/activerecord-multi-tenant/sidekiq_spec.rb b/spec/activerecord-multi-tenant/sidekiq_spec.rb index 7307e8a7..20615c5c 100644 --- a/spec/activerecord-multi-tenant/sidekiq_spec.rb +++ b/spec/activerecord-multi-tenant/sidekiq_spec.rb @@ -5,6 +5,9 @@ describe MultiTenant, 'Sidekiq' do let(:server) { Sidekiq::Middleware::MultiTenant::Server.new } let(:account) { Account.create(name: 'test') } + let(:deleted_acount) { Account.create(name: 'deleted') } + + before { deleted_acount.destroy! } describe 'server middleware' do it 'sets the multitenant context when provided in message' do @@ -15,6 +18,14 @@ end end + it 'sets the multitenant context (id) even if tenant not found' do + server.call(double,{'bogus' => 'message', + 'multi_tenant' => { 'class' => deleted_acount.class.name, 'id' => deleted_acount.id}}, + 'bogus_queue') do + expect(MultiTenant.current_tenant).to eq(deleted_acount.id) + end + end + it 'does not set the multitenant context when no tenant provided' do server.call(double, {'bogus' => 'message'}, 'bogus_queue') do expect(MultiTenant.current_tenant).to be_nil From 7679ee1bb57c7b4e8cec1287767875262fee132c Mon Sep 17 00:00:00 2001 From: alpaca-tc Date: Thu, 23 Dec 2021 15:46:15 +0900 Subject: [PATCH 002/105] Bump up mimemagic mimemagic 0.3.5 is already removed from rubygems --- Gemfile.lock | 6 ++++-- gemfiles/active_record_6.0.gemfile.lock | 4 +++- gemfiles/active_record_6.1.gemfile.lock | 4 +++- gemfiles/rails_5.2.gemfile.lock | 4 +++- gemfiles/rails_6.0.gemfile.lock | 4 +++- gemfiles/rails_6.1.gemfile.lock | 4 +++- 6 files changed, 19 insertions(+), 7 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index b4f35e05..8f1b3871 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -87,7 +87,9 @@ GEM marcel (0.3.3) mimemagic (~> 0.3.2) method_source (0.9.2) - mimemagic (0.3.5) + mimemagic (0.3.10) + nokogiri (~> 1) + rake mini_mime (1.0.2) mini_portile2 (2.5.0) minitest (5.14.2) @@ -196,4 +198,4 @@ DEPENDENCIES thor BUNDLED WITH - 2.1.4 + 2.2.22 diff --git a/gemfiles/active_record_6.0.gemfile.lock b/gemfiles/active_record_6.0.gemfile.lock index 34242ca8..7fb9e1e0 100644 --- a/gemfiles/active_record_6.0.gemfile.lock +++ b/gemfiles/active_record_6.0.gemfile.lock @@ -87,7 +87,9 @@ GEM marcel (0.3.3) mimemagic (~> 0.3.2) method_source (1.0.0) - mimemagic (0.3.5) + mimemagic (0.3.10) + nokogiri (~> 1) + rake mini_mime (1.0.2) mini_portile2 (2.4.0) minitest (5.14.1) diff --git a/gemfiles/active_record_6.1.gemfile.lock b/gemfiles/active_record_6.1.gemfile.lock index eb6b5c43..247e751a 100644 --- a/gemfiles/active_record_6.1.gemfile.lock +++ b/gemfiles/active_record_6.1.gemfile.lock @@ -91,7 +91,9 @@ GEM marcel (0.3.3) mimemagic (~> 0.3.2) method_source (1.0.0) - mimemagic (0.3.5) + mimemagic (0.3.10) + nokogiri (~> 1) + rake mini_mime (1.0.2) mini_portile2 (2.4.0) minitest (5.14.2) diff --git a/gemfiles/rails_5.2.gemfile.lock b/gemfiles/rails_5.2.gemfile.lock index ffcc57a4..0238f184 100644 --- a/gemfiles/rails_5.2.gemfile.lock +++ b/gemfiles/rails_5.2.gemfile.lock @@ -74,7 +74,9 @@ GEM marcel (0.3.3) mimemagic (~> 0.3.2) method_source (1.0.0) - mimemagic (0.3.5) + mimemagic (0.3.10) + nokogiri (~> 1) + rake mini_mime (1.0.2) mini_portile2 (2.1.0) minitest (5.14.1) diff --git a/gemfiles/rails_6.0.gemfile.lock b/gemfiles/rails_6.0.gemfile.lock index 4710efad..0fb70ce3 100644 --- a/gemfiles/rails_6.0.gemfile.lock +++ b/gemfiles/rails_6.0.gemfile.lock @@ -87,7 +87,9 @@ GEM marcel (0.3.3) mimemagic (~> 0.3.2) method_source (1.0.0) - mimemagic (0.3.5) + mimemagic (0.3.10) + nokogiri (~> 1) + rake mini_mime (1.0.2) mini_portile2 (2.4.0) minitest (5.14.1) diff --git a/gemfiles/rails_6.1.gemfile.lock b/gemfiles/rails_6.1.gemfile.lock index a4c29d39..f4d27b24 100644 --- a/gemfiles/rails_6.1.gemfile.lock +++ b/gemfiles/rails_6.1.gemfile.lock @@ -91,7 +91,9 @@ GEM marcel (0.3.3) mimemagic (~> 0.3.2) method_source (1.0.0) - mimemagic (0.3.5) + mimemagic (0.3.10) + nokogiri (~> 1) + rake mini_mime (1.0.2) mini_portile2 (2.4.0) minitest (5.14.2) From 082837d784abbe3167427cac1795d6ba72c648d8 Mon Sep 17 00:00:00 2001 From: alpaca-tc Date: Thu, 23 Dec 2021 16:00:29 +0900 Subject: [PATCH 003/105] Bump up minitest minitest 5.14.1 requires ruby 2.2 but it is not tested version. --- Gemfile.lock | 2 +- gemfiles/active_record_5.2.gemfile.lock | 8 +++++--- gemfiles/active_record_6.0.gemfile.lock | 14 ++++++++------ gemfiles/active_record_6.1.gemfile.lock | 14 ++++++++------ gemfiles/rails_5.2.gemfile.lock | 4 ++-- gemfiles/rails_6.0.gemfile.lock | 14 ++++++++------ gemfiles/rails_6.1.gemfile.lock | 14 ++++++++------ 7 files changed, 40 insertions(+), 30 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 8f1b3871..1ac5d5b9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -92,7 +92,7 @@ GEM rake mini_mime (1.0.2) mini_portile2 (2.5.0) - minitest (5.14.2) + minitest (5.14.4) nio4r (2.5.4) nokogiri (1.11.1) mini_portile2 (~> 2.5.0) diff --git a/gemfiles/active_record_5.2.gemfile.lock b/gemfiles/active_record_5.2.gemfile.lock index 6e9e049b..18b346dc 100644 --- a/gemfiles/active_record_5.2.gemfile.lock +++ b/gemfiles/active_record_5.2.gemfile.lock @@ -74,10 +74,12 @@ GEM marcel (0.3.3) mimemagic (~> 0.3.2) method_source (1.0.0) - mimemagic (0.3.5) + mimemagic (0.3.10) + nokogiri (~> 1) + rake mini_mime (1.0.2) mini_portile2 (2.1.0) - minitest (5.14.1) + minitest (5.14.4) nio4r (2.3.1) nokogiri (1.7.2) mini_portile2 (~> 2.1.0) @@ -185,4 +187,4 @@ DEPENDENCIES thor BUNDLED WITH - 2.1.4 + 2.2.22 diff --git a/gemfiles/active_record_6.0.gemfile.lock b/gemfiles/active_record_6.0.gemfile.lock index 7fb9e1e0..d5b57876 100644 --- a/gemfiles/active_record_6.0.gemfile.lock +++ b/gemfiles/active_record_6.0.gemfile.lock @@ -91,11 +91,12 @@ GEM nokogiri (~> 1) rake mini_mime (1.0.2) - mini_portile2 (2.4.0) - minitest (5.14.1) + mini_portile2 (2.6.1) + minitest (5.14.4) nio4r (2.5.4) - nokogiri (1.10.9) - mini_portile2 (~> 2.4.0) + nokogiri (1.12.5) + mini_portile2 (~> 2.6.1) + racc (~> 1.4) pg (1.2.3) pry (0.13.1) coderay (~> 1.1) @@ -103,6 +104,7 @@ GEM pry-byebug (3.9.0) byebug (~> 11.0) pry (~> 0.13.0) + racc (1.6.0) rack (2.2.2) rack-protection (2.0.8.1) rack @@ -134,7 +136,7 @@ GEM method_source rake (>= 0.8.7) thor (>= 0.20.3, < 2.0) - rake (13.0.1) + rake (13.0.6) redis (4.1.4) request_store (1.5.0) rack (>= 1.4) @@ -197,4 +199,4 @@ DEPENDENCIES thor BUNDLED WITH - 2.1.4 + 2.2.22 diff --git a/gemfiles/active_record_6.1.gemfile.lock b/gemfiles/active_record_6.1.gemfile.lock index 247e751a..a2ce68d8 100644 --- a/gemfiles/active_record_6.1.gemfile.lock +++ b/gemfiles/active_record_6.1.gemfile.lock @@ -95,11 +95,12 @@ GEM nokogiri (~> 1) rake mini_mime (1.0.2) - mini_portile2 (2.4.0) - minitest (5.14.2) + mini_portile2 (2.6.1) + minitest (5.14.4) nio4r (2.5.4) - nokogiri (1.10.10) - mini_portile2 (~> 2.4.0) + nokogiri (1.12.5) + mini_portile2 (~> 2.6.1) + racc (~> 1.4) pg (1.2.3) pry (0.13.1) coderay (~> 1.1) @@ -107,6 +108,7 @@ GEM pry-byebug (3.9.0) byebug (~> 11.0) pry (~> 0.13.0) + racc (1.6.0) rack (2.2.3) rack-test (1.1.0) rack (>= 1.0, < 3) @@ -136,7 +138,7 @@ GEM method_source rake (>= 0.8.7) thor (~> 1.0) - rake (13.0.3) + rake (13.0.6) redis (4.2.5) request_store (1.5.0) rack (>= 1.4) @@ -197,4 +199,4 @@ DEPENDENCIES thor BUNDLED WITH - 2.1.4 + 2.2.22 diff --git a/gemfiles/rails_5.2.gemfile.lock b/gemfiles/rails_5.2.gemfile.lock index 0238f184..066002b1 100644 --- a/gemfiles/rails_5.2.gemfile.lock +++ b/gemfiles/rails_5.2.gemfile.lock @@ -79,7 +79,7 @@ GEM rake mini_mime (1.0.2) mini_portile2 (2.1.0) - minitest (5.14.1) + minitest (5.14.4) nio4r (2.3.1) nokogiri (1.7.2) mini_portile2 (~> 2.1.0) @@ -187,4 +187,4 @@ DEPENDENCIES thor BUNDLED WITH - 2.1.4 + 2.2.22 diff --git a/gemfiles/rails_6.0.gemfile.lock b/gemfiles/rails_6.0.gemfile.lock index 0fb70ce3..573ce8cc 100644 --- a/gemfiles/rails_6.0.gemfile.lock +++ b/gemfiles/rails_6.0.gemfile.lock @@ -91,11 +91,12 @@ GEM nokogiri (~> 1) rake mini_mime (1.0.2) - mini_portile2 (2.4.0) - minitest (5.14.1) + mini_portile2 (2.6.1) + minitest (5.14.4) nio4r (2.5.2) - nokogiri (1.10.9) - mini_portile2 (~> 2.4.0) + nokogiri (1.12.5) + mini_portile2 (~> 2.6.1) + racc (~> 1.4) pg (1.2.3) pry (0.13.1) coderay (~> 1.1) @@ -103,6 +104,7 @@ GEM pry-byebug (3.9.0) byebug (~> 11.0) pry (~> 0.13.0) + racc (1.6.0) rack (2.2.2) rack-protection (2.0.8.1) rack @@ -134,7 +136,7 @@ GEM method_source rake (>= 0.8.7) thor (>= 0.20.3, < 2.0) - rake (13.0.1) + rake (13.0.6) redis (4.1.4) request_store (1.5.0) rack (>= 1.4) @@ -197,4 +199,4 @@ DEPENDENCIES thor BUNDLED WITH - 2.1.4 + 2.2.22 diff --git a/gemfiles/rails_6.1.gemfile.lock b/gemfiles/rails_6.1.gemfile.lock index f4d27b24..03a1fe9f 100644 --- a/gemfiles/rails_6.1.gemfile.lock +++ b/gemfiles/rails_6.1.gemfile.lock @@ -95,11 +95,12 @@ GEM nokogiri (~> 1) rake mini_mime (1.0.2) - mini_portile2 (2.4.0) - minitest (5.14.2) + mini_portile2 (2.6.1) + minitest (5.14.4) nio4r (2.5.4) - nokogiri (1.10.10) - mini_portile2 (~> 2.4.0) + nokogiri (1.12.5) + mini_portile2 (~> 2.6.1) + racc (~> 1.4) pg (1.2.3) pry (0.13.1) coderay (~> 1.1) @@ -107,6 +108,7 @@ GEM pry-byebug (3.9.0) byebug (~> 11.0) pry (~> 0.13.0) + racc (1.6.0) rack (2.2.3) rack-test (1.1.0) rack (>= 1.0, < 3) @@ -136,7 +138,7 @@ GEM method_source rake (>= 0.8.7) thor (~> 1.0) - rake (13.0.3) + rake (13.0.6) redis (4.2.5) request_store (1.5.0) rack (>= 1.4) @@ -197,4 +199,4 @@ DEPENDENCIES thor BUNDLED WITH - 2.1.4 + 2.2.22 From 8f12718da37bb1177c09a8e1b0df0aaf66ef998e Mon Sep 17 00:00:00 2001 From: alpaca-tc Date: Thu, 23 Dec 2021 16:20:35 +0900 Subject: [PATCH 004/105] Fixed spec for rails7 --- lib/activerecord-multi-tenant/query_rewriter.rb | 2 ++ spec/activerecord-multi-tenant/model_extensions_spec.rb | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/activerecord-multi-tenant/query_rewriter.rb b/lib/activerecord-multi-tenant/query_rewriter.rb index f637752a..91619be6 100644 --- a/lib/activerecord-multi-tenant/query_rewriter.rb +++ b/lib/activerecord-multi-tenant/query_rewriter.rb @@ -122,6 +122,8 @@ def visit_Arel_Nodes_OuterJoin(o, collector = nil) alias :visit_Arel_Nodes_FullOuterJoin :visit_Arel_Nodes_OuterJoin alias :visit_Arel_Nodes_RightOuterJoin :visit_Arel_Nodes_OuterJoin + alias :visit_ActiveModel_Attribute :terminal + private def tenant_relation?(table_name) diff --git a/spec/activerecord-multi-tenant/model_extensions_spec.rb b/spec/activerecord-multi-tenant/model_extensions_spec.rb index 00e3bf33..47071631 100644 --- a/spec/activerecord-multi-tenant/model_extensions_spec.rb +++ b/spec/activerecord-multi-tenant/model_extensions_spec.rb @@ -412,7 +412,7 @@ account1 = Account.create! name: 'Account 1' category1 = Category.create! name: 'Category 1' - expected_sql = if uses_prepared_statements? && (ActiveRecord::VERSION::MAJOR == 5 || (ActiveRecord::VERSION::MAJOR == 6 && ActiveRecord::VERSION::MINOR >= 1)) + expected_sql = if uses_prepared_statements? && (ActiveRecord::VERSION::MAJOR == 5 || (ActiveRecord::VERSION::MAJOR == 6 && ActiveRecord::VERSION::MINOR >= 1) || ActiveRecord::VERSION::MAJOR == 7) <<-sql SELECT "projects"."id" AS t0_r0, "projects"."account_id" AS t0_r1, "projects"."name" AS t0_r2, "categories"."id" AS t1_r0, "categories"."name" AS t1_r1 FROM "projects" LEFT OUTER JOIN "project_categories" ON "project_categories"."project_id" = "projects"."id" AND "project_categories"."account_id" = 1 AND "projects"."account_id" = 1 LEFT OUTER JOIN "categories" ON "categories"."id" = "project_categories"."category_id" AND "project_categories"."account_id" = 1 WHERE "projects"."account_id" = 1 sql @@ -451,7 +451,7 @@ category1 = Category.create! name: 'Category 1' MultiTenant.with(account1) do - expected_sql = if uses_prepared_statements? && (ActiveRecord::VERSION::MAJOR == 5 || (ActiveRecord::VERSION::MAJOR == 6 && ActiveRecord::VERSION::MINOR >= 1)) + expected_sql = if uses_prepared_statements? && (ActiveRecord::VERSION::MAJOR == 5 || (ActiveRecord::VERSION::MAJOR == 6 && ActiveRecord::VERSION::MINOR >= 1) || ActiveRecord::VERSION::MAJOR == 7) <<-sql SELECT "tasks".* FROM "tasks" INNER JOIN "projects" ON "projects"."id" = "tasks"."project_id" AND "projects"."account_id" = 1 LEFT JOIN project_categories pc ON project.category_id = pc.id WHERE "tasks"."account_id" = 1 sql From a91f00873ac7341ae4646757fc236486deac0c5b Mon Sep 17 00:00:00 2001 From: alpaca-tc Date: Thu, 23 Dec 2021 16:14:21 +0900 Subject: [PATCH 005/105] Testing with AR 7.0 --- .travis.yml | 18 ++- Appraisals | 8 + gemfiles/active_record_7.0.gemfile | 8 + gemfiles/active_record_7.0.gemfile.lock | 189 ++++++++++++++++++++++++ gemfiles/rails_7.0.gemfile | 8 + gemfiles/rails_7.0.gemfile.lock | 189 ++++++++++++++++++++++++ 6 files changed, 417 insertions(+), 3 deletions(-) create mode 100644 gemfiles/active_record_7.0.gemfile create mode 100644 gemfiles/active_record_7.0.gemfile.lock create mode 100644 gemfiles/rails_7.0.gemfile create mode 100644 gemfiles/rails_7.0.gemfile.lock diff --git a/.travis.yml b/.travis.yml index 6e5bd5e9..b564eb84 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,17 +4,19 @@ cache: bundler language: ruby rvm: - - 2.5.8 - - 2.6.4 - - 2.7.1 + - 2.5.9 + - 2.6.9 + - 2.7.5 gemfile: - gemfiles/rails_5.2.gemfile - gemfiles/rails_6.0.gemfile - gemfiles/rails_6.1.gemfile + - gemfiles/rails_7.0.gemfile - gemfiles/active_record_5.2.gemfile - gemfiles/active_record_6.0.gemfile - gemfiles/active_record_6.1.gemfile + - gemfiles/active_record_7.0.gemfile env: - PREPARED_STATEMENTS=0 @@ -22,6 +24,16 @@ env: matrix: fast_finish: true + exclude: + # Rails7 supports only >= 2.7.0 + - rvm: 2.5.9 + gemfile: gemfiles/rails_7.0.gemfile + - rvm: 2.5.9 + gemfile: gemfiles/active_record_7.0.gemfile + - rvm: 2.6.9 + gemfile: gemfiles/rails_7.0.gemfile + - rvm: 2.6.9 + gemfile: gemfiles/active_record_7.0.gemfile services: - docker diff --git a/Appraisals b/Appraisals index 213c64d7..bb141136 100644 --- a/Appraisals +++ b/Appraisals @@ -18,6 +18,10 @@ appraise 'rails-6.1' do gem 'rails', '~> 6.1.0' end +appraise 'rails-7.0' do + gem 'rails', '~> 7.0.0' +end + appraise 'active-record-5.2' do gem 'activerecord', '~> 5.2.0' gem 'i18n', '~> 0.9.5' @@ -37,3 +41,7 @@ end appraise 'active-record-6.1' do gem 'activerecord', '~> 6.1.0' end + +appraise 'active-record-7.0' do + gem 'activerecord', '~> 7.0.0' +end diff --git a/gemfiles/active_record_7.0.gemfile b/gemfiles/active_record_7.0.gemfile new file mode 100644 index 00000000..76f57c2e --- /dev/null +++ b/gemfiles/active_record_7.0.gemfile @@ -0,0 +1,8 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "appraisal" +gem "activerecord", "~> 7.0.0" + +gemspec path: "../" diff --git a/gemfiles/active_record_7.0.gemfile.lock b/gemfiles/active_record_7.0.gemfile.lock new file mode 100644 index 00000000..d0e12e45 --- /dev/null +++ b/gemfiles/active_record_7.0.gemfile.lock @@ -0,0 +1,189 @@ +PATH + remote: .. + specs: + activerecord-multi-tenant (1.1.1) + rails (>= 4.2) + request_store (>= 1.0.5) + +GEM + remote: https://rubygems.org/ + specs: + actioncable (7.0.0) + actionpack (= 7.0.0) + activesupport (= 7.0.0) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + actionmailbox (7.0.0) + actionpack (= 7.0.0) + activejob (= 7.0.0) + activerecord (= 7.0.0) + activestorage (= 7.0.0) + activesupport (= 7.0.0) + mail (>= 2.7.1) + actionmailer (7.0.0) + actionpack (= 7.0.0) + actionview (= 7.0.0) + activejob (= 7.0.0) + activesupport (= 7.0.0) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 2.0) + actionpack (7.0.0) + actionview (= 7.0.0) + activesupport (= 7.0.0) + rack (~> 2.0, >= 2.2.0) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (7.0.0) + actionpack (= 7.0.0) + activerecord (= 7.0.0) + activestorage (= 7.0.0) + activesupport (= 7.0.0) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (7.0.0) + activesupport (= 7.0.0) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + activejob (7.0.0) + activesupport (= 7.0.0) + globalid (>= 0.3.6) + activemodel (7.0.0) + activesupport (= 7.0.0) + activerecord (7.0.0) + activemodel (= 7.0.0) + activesupport (= 7.0.0) + activestorage (7.0.0) + actionpack (= 7.0.0) + activejob (= 7.0.0) + activerecord (= 7.0.0) + activesupport (= 7.0.0) + marcel (~> 1.0) + mini_mime (>= 1.1.0) + activesupport (7.0.0) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + appraisal (2.4.1) + bundler + rake + thor (>= 0.14.0) + builder (3.2.4) + byebug (11.1.3) + coderay (1.1.3) + concurrent-ruby (1.1.9) + connection_pool (2.2.5) + crass (1.0.6) + diff-lcs (1.4.4) + erubi (1.10.0) + globalid (1.0.0) + activesupport (>= 5.0) + i18n (1.8.11) + concurrent-ruby (~> 1.0) + loofah (2.13.0) + crass (~> 1.0.2) + nokogiri (>= 1.5.9) + mail (2.7.1) + mini_mime (>= 0.1.1) + marcel (1.0.2) + method_source (1.0.0) + mini_mime (1.1.2) + minitest (5.15.0) + nio4r (2.5.8) + nokogiri (1.12.5-x86_64-darwin) + racc (~> 1.4) + pg (1.2.3) + pry (0.13.1) + coderay (~> 1.1) + method_source (~> 1.0) + pry-byebug (3.9.0) + byebug (~> 11.0) + pry (~> 0.13.0) + racc (1.6.0) + rack (2.2.3) + rack-test (1.1.0) + rack (>= 1.0, < 3) + rails (7.0.0) + actioncable (= 7.0.0) + actionmailbox (= 7.0.0) + actionmailer (= 7.0.0) + actionpack (= 7.0.0) + actiontext (= 7.0.0) + actionview (= 7.0.0) + activejob (= 7.0.0) + activemodel (= 7.0.0) + activerecord (= 7.0.0) + activestorage (= 7.0.0) + activesupport (= 7.0.0) + bundler (>= 1.15.0) + railties (= 7.0.0) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.4.2) + loofah (~> 2.3) + railties (7.0.0) + actionpack (= 7.0.0) + activesupport (= 7.0.0) + method_source + rake (>= 12.2) + thor (~> 1.0) + zeitwerk (~> 2.5) + rake (13.0.6) + redis (4.5.1) + request_store (1.5.0) + rack (>= 1.4) + rspec (3.10.0) + rspec-core (~> 3.10.0) + rspec-expectations (~> 3.10.0) + rspec-mocks (~> 3.10.0) + rspec-core (3.10.1) + rspec-support (~> 3.10.0) + rspec-expectations (3.10.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-mocks (3.10.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-rails (5.0.2) + actionpack (>= 5.2) + activesupport (>= 5.2) + railties (>= 5.2) + rspec-core (~> 3.10) + rspec-expectations (~> 3.10) + rspec-mocks (~> 3.10) + rspec-support (~> 3.10) + rspec-support (3.10.3) + sidekiq (6.3.1) + connection_pool (>= 2.2.2) + rack (~> 2.0) + redis (>= 4.2.0) + thor (1.1.0) + tzinfo (2.0.4) + concurrent-ruby (~> 1.0) + websocket-driver (0.7.5) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + zeitwerk (2.5.1) + +PLATFORMS + ruby + +DEPENDENCIES + activerecord (~> 7.0.0) + activerecord-multi-tenant! + appraisal + pg + pry + pry-byebug + rake + rspec (>= 3.0) + rspec-rails + sidekiq + thor + +BUNDLED WITH + 2.2.22 diff --git a/gemfiles/rails_7.0.gemfile b/gemfiles/rails_7.0.gemfile new file mode 100644 index 00000000..75887596 --- /dev/null +++ b/gemfiles/rails_7.0.gemfile @@ -0,0 +1,8 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "appraisal" +gem "rails", "~> 7.0.0" + +gemspec path: "../" diff --git a/gemfiles/rails_7.0.gemfile.lock b/gemfiles/rails_7.0.gemfile.lock new file mode 100644 index 00000000..cf86a620 --- /dev/null +++ b/gemfiles/rails_7.0.gemfile.lock @@ -0,0 +1,189 @@ +PATH + remote: .. + specs: + activerecord-multi-tenant (1.1.1) + rails (>= 4.2) + request_store (>= 1.0.5) + +GEM + remote: https://rubygems.org/ + specs: + actioncable (7.0.0) + actionpack (= 7.0.0) + activesupport (= 7.0.0) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + actionmailbox (7.0.0) + actionpack (= 7.0.0) + activejob (= 7.0.0) + activerecord (= 7.0.0) + activestorage (= 7.0.0) + activesupport (= 7.0.0) + mail (>= 2.7.1) + actionmailer (7.0.0) + actionpack (= 7.0.0) + actionview (= 7.0.0) + activejob (= 7.0.0) + activesupport (= 7.0.0) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 2.0) + actionpack (7.0.0) + actionview (= 7.0.0) + activesupport (= 7.0.0) + rack (~> 2.0, >= 2.2.0) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (7.0.0) + actionpack (= 7.0.0) + activerecord (= 7.0.0) + activestorage (= 7.0.0) + activesupport (= 7.0.0) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (7.0.0) + activesupport (= 7.0.0) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + activejob (7.0.0) + activesupport (= 7.0.0) + globalid (>= 0.3.6) + activemodel (7.0.0) + activesupport (= 7.0.0) + activerecord (7.0.0) + activemodel (= 7.0.0) + activesupport (= 7.0.0) + activestorage (7.0.0) + actionpack (= 7.0.0) + activejob (= 7.0.0) + activerecord (= 7.0.0) + activesupport (= 7.0.0) + marcel (~> 1.0) + mini_mime (>= 1.1.0) + activesupport (7.0.0) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + appraisal (2.4.1) + bundler + rake + thor (>= 0.14.0) + builder (3.2.4) + byebug (11.1.3) + coderay (1.1.3) + concurrent-ruby (1.1.9) + connection_pool (2.2.5) + crass (1.0.6) + diff-lcs (1.4.4) + erubi (1.10.0) + globalid (1.0.0) + activesupport (>= 5.0) + i18n (1.8.11) + concurrent-ruby (~> 1.0) + loofah (2.13.0) + crass (~> 1.0.2) + nokogiri (>= 1.5.9) + mail (2.7.1) + mini_mime (>= 0.1.1) + marcel (1.0.2) + method_source (1.0.0) + mini_mime (1.1.2) + minitest (5.15.0) + nio4r (2.5.8) + nokogiri (1.12.5-x86_64-darwin) + racc (~> 1.4) + pg (1.2.3) + pry (0.13.1) + coderay (~> 1.1) + method_source (~> 1.0) + pry-byebug (3.9.0) + byebug (~> 11.0) + pry (~> 0.13.0) + racc (1.6.0) + rack (2.2.3) + rack-test (1.1.0) + rack (>= 1.0, < 3) + rails (7.0.0) + actioncable (= 7.0.0) + actionmailbox (= 7.0.0) + actionmailer (= 7.0.0) + actionpack (= 7.0.0) + actiontext (= 7.0.0) + actionview (= 7.0.0) + activejob (= 7.0.0) + activemodel (= 7.0.0) + activerecord (= 7.0.0) + activestorage (= 7.0.0) + activesupport (= 7.0.0) + bundler (>= 1.15.0) + railties (= 7.0.0) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.4.2) + loofah (~> 2.3) + railties (7.0.0) + actionpack (= 7.0.0) + activesupport (= 7.0.0) + method_source + rake (>= 12.2) + thor (~> 1.0) + zeitwerk (~> 2.5) + rake (13.0.6) + redis (4.5.1) + request_store (1.5.0) + rack (>= 1.4) + rspec (3.10.0) + rspec-core (~> 3.10.0) + rspec-expectations (~> 3.10.0) + rspec-mocks (~> 3.10.0) + rspec-core (3.10.1) + rspec-support (~> 3.10.0) + rspec-expectations (3.10.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-mocks (3.10.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-rails (5.0.2) + actionpack (>= 5.2) + activesupport (>= 5.2) + railties (>= 5.2) + rspec-core (~> 3.10) + rspec-expectations (~> 3.10) + rspec-mocks (~> 3.10) + rspec-support (~> 3.10) + rspec-support (3.10.3) + sidekiq (6.3.1) + connection_pool (>= 2.2.2) + rack (~> 2.0) + redis (>= 4.2.0) + thor (1.1.0) + tzinfo (2.0.4) + concurrent-ruby (~> 1.0) + websocket-driver (0.7.5) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + zeitwerk (2.5.1) + +PLATFORMS + ruby + +DEPENDENCIES + activerecord-multi-tenant! + appraisal + pg + pry + pry-byebug + rails (~> 7.0.0) + rake + rspec (>= 3.0) + rspec-rails + sidekiq + thor + +BUNDLED WITH + 2.2.22 From 61071128a74f243c6ed65c2e4ee60eeffa24aa75 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Feb 2022 13:28:33 +0000 Subject: [PATCH 006/105] Bump nokogiri from 1.11.1 to 1.13.3 Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.11.1 to 1.13.3. - [Release notes](https://github.com/sparklemotion/nokogiri/releases) - [Changelog](https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md) - [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.11.1...v1.13.3) --- updated-dependencies: - dependency-name: nokogiri dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 1ac5d5b9..7f49e62e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -91,11 +91,11 @@ GEM nokogiri (~> 1) rake mini_mime (1.0.2) - mini_portile2 (2.5.0) + mini_portile2 (2.8.0) minitest (5.14.4) nio4r (2.5.4) - nokogiri (1.11.1) - mini_portile2 (~> 2.5.0) + nokogiri (1.13.3) + mini_portile2 (~> 2.8.0) racc (~> 1.4) pg (1.1.4) pry (0.12.2) @@ -104,7 +104,7 @@ GEM pry-byebug (3.7.0) byebug (~> 11.0) pry (~> 0.10) - racc (1.5.2) + racc (1.6.0) rack (2.2.3) rack-protection (2.0.5) rack From 598e8a6249b83f6a0203fc13a644a71a60579a95 Mon Sep 17 00:00:00 2001 From: Matt Larraz Date: Thu, 11 Feb 2021 14:37:29 -0500 Subject: [PATCH 007/105] Use Github Actions for CI --- .github/workflows/CI.yml | 40 ++++++++++++++++++++++++++++++++++++++++ .rspec | 1 + 2 files changed, 41 insertions(+) create mode 100644 .github/workflows/CI.yml create mode 100644 .rspec diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 00000000..99b32503 --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,40 @@ +name: CI + +on: + push: + branches: + - master + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + ruby: + - '2.5' + - '2.6' + - '2.7' + gemfile: + - rails_5.2 + - rails_6.0 + - rails_6.1 + - active_record_5.2 + - active_record_6.0 + - active_record_6.1 + prepared_statements: [true, false] + name: Ruby ${{ matrix.ruby }} / ${{ matrix.gemfile }} ${{ (matrix.prepared_statements && 'w/ prepared statements') || '' }} + env: + BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile + PREPARED_STATEMENTS: ${{ matrix.prepared_statements && '1' }} + steps: + - uses: actions/checkout@v2 + - run: | + docker-compose up -d + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + - run: | + bundle exec rake spec diff --git a/.rspec b/.rspec new file mode 100644 index 00000000..18ed8e15 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--force-color From d69b93193157a6855eb9355dc7abbf522e4a6db0 Mon Sep 17 00:00:00 2001 From: Sergio Medina Date: Wed, 10 Feb 2021 12:17:16 +0100 Subject: [PATCH 008/105] fix incorrect SQL generated when joining two models and one has a default scope --- .../query_rewriter.rb | 6 +- .../model_extensions_spec.rb | 98 +++++++++---------- .../query_rewriter_spec.rb | 17 ++++ spec/schema.rb | 31 +++++- 4 files changed, 97 insertions(+), 55 deletions(-) diff --git a/lib/activerecord-multi-tenant/query_rewriter.rb b/lib/activerecord-multi-tenant/query_rewriter.rb index 91619be6..707f8603 100644 --- a/lib/activerecord-multi-tenant/query_rewriter.rb +++ b/lib/activerecord-multi-tenant/query_rewriter.rb @@ -276,7 +276,11 @@ def build_arel(*args) if node.wheres.empty? node.wheres = [enforcement_clause] else - node.wheres[0] = enforcement_clause.and(node.wheres[0]) + if node.wheres[0].is_a?(Arel::Nodes::And) + node.wheres[0].children << enforcement_clause + else + node.wheres[0] = enforcement_clause.and(node.wheres[0]) + end end else raise "UnknownContext" diff --git a/spec/activerecord-multi-tenant/model_extensions_spec.rb b/spec/activerecord-multi-tenant/model_extensions_spec.rb index 47071631..63db21e2 100644 --- a/spec/activerecord-multi-tenant/model_extensions_spec.rb +++ b/spec/activerecord-multi-tenant/model_extensions_spec.rb @@ -348,9 +348,12 @@ end it "applies the team_id conditions in the where clause" do - expected_sql = <<-sql - SELECT "sub_tasks".* FROM "sub_tasks" INNER JOIN "tasks" ON "sub_tasks"."task_id" = "tasks"."id" AND "sub_tasks"."account_id" = "tasks"."account_id" WHERE "tasks"."account_id" = 1 AND "sub_tasks"."account_id" = 1 AND "tasks"."project_id" = 1 - sql + option1 = <<-sql.strip + SELECT "sub_tasks".* FROM "sub_tasks" INNER JOIN "tasks" ON "sub_tasks"."task_id" = "tasks"."id" AND "sub_tasks"."account_id" = "tasks"."account_id" WHERE "tasks"."project_id" = 1 AND "sub_tasks"."account_id" = 1 AND "tasks"."account_id" = 1 + sql + option2 = <<-sql.strip + SELECT "sub_tasks".* FROM "sub_tasks" INNER JOIN "tasks" ON "sub_tasks"."task_id" = "tasks"."id" AND "sub_tasks"."account_id" = "tasks"."account_id" WHERE "sub_tasks"."account_id" = 1 AND "tasks"."project_id" = 1 AND "tasks"."account_id" = 1 + sql account1 = Account.create! name: 'Account 1' @@ -358,7 +361,7 @@ project1 = Project.create! name: 'Project 1' task1 = Task.create! name: 'Task 1', project: project1 subtask1 = SubTask.create! task: task1 - expect(project1.sub_tasks.to_sql).to eq(expected_sql.strip) + expect(project1.sub_tasks.to_sql).to eq(option1).or(eq(option2)) expect(project1.sub_tasks).to include(subtask1) end @@ -373,9 +376,13 @@ end it "tests joins between distributed and reference table" do - expected_sql = <<-sql - SELECT "categories".* FROM "categories" INNER JOIN "project_categories" ON "categories"."id" = "project_categories"."category_id" WHERE "project_categories"."account_id" = 1 AND "project_categories"."project_id" = 1 - sql + option1 = <<-sql.strip + SELECT "categories".* FROM "categories" INNER JOIN "project_categories" ON "categories"."id" = "project_categories"."category_id" WHERE "project_categories"."project_id" = 1 AND "project_categories"."account_id" = 1 + sql + option2 = <<-sql.strip + SELECT "categories".* FROM "categories" INNER JOIN "project_categories" ON "categories"."id" = "project_categories"."category_id" WHERE "project_categories"."account_id" = 1 AND "project_categories"."project_id" = 1 + sql + account1 = Account.create! name: 'Account 1' category1 = Category.create! name: 'Category 1' @@ -383,7 +390,7 @@ project1 = Project.create! name: 'Project 1' projectcategory = ProjectCategory.create! name: 'project cat 1', project: project1, category: category1 - expect(project1.categories.to_sql).to eq(expected_sql.strip) + expect(project1.categories.to_sql).to eq(option1).or(eq(option2)) expect(project1.categories).to include(category1) expect(project1.project_categories).to include(projectcategory) end @@ -412,21 +419,18 @@ account1 = Account.create! name: 'Account 1' category1 = Category.create! name: 'Category 1' - expected_sql = if uses_prepared_statements? && (ActiveRecord::VERSION::MAJOR == 5 || (ActiveRecord::VERSION::MAJOR == 6 && ActiveRecord::VERSION::MINOR >= 1) || ActiveRecord::VERSION::MAJOR == 7) - <<-sql - SELECT "projects"."id" AS t0_r0, "projects"."account_id" AS t0_r1, "projects"."name" AS t0_r2, "categories"."id" AS t1_r0, "categories"."name" AS t1_r1 FROM "projects" LEFT OUTER JOIN "project_categories" ON "project_categories"."project_id" = "projects"."id" AND "project_categories"."account_id" = 1 AND "projects"."account_id" = 1 LEFT OUTER JOIN "categories" ON "categories"."id" = "project_categories"."category_id" AND "project_categories"."account_id" = 1 WHERE "projects"."account_id" = 1 - sql - else - <<-sql - 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"."account_id" = 1 AND "project_categories"."project_id" = "projects"."id" AND "projects"."account_id" = 1 LEFT OUTER JOIN "categories" ON "categories"."id" = "project_categories"."category_id" AND "project_categories"."account_id" = 1 WHERE "projects"."account_id" = 1 - sql - end + option1 = <<-sql.strip + SELECT "projects"."id" AS t0_r0, "projects"."account_id" AS t0_r1, "projects"."name" AS t0_r2, "categories"."id" AS t1_r0, "categories"."name" AS t1_r1 FROM "projects" LEFT OUTER JOIN "project_categories" ON "project_categories"."project_id" = "projects"."id" AND "project_categories"."account_id" = 1 AND "projects"."account_id" = 1 LEFT OUTER JOIN "categories" ON "categories"."id" = "project_categories"."category_id" AND "project_categories"."account_id" = 1 WHERE "projects"."account_id" = 1 + sql + option2 = <<-sql.strip + 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"."account_id" = 1 AND "project_categories"."project_id" = "projects"."id" AND "projects"."account_id" = 1 LEFT OUTER JOIN "categories" ON "categories"."id" = "project_categories"."category_id" AND "project_categories"."account_id" = 1 WHERE "projects"."account_id" = 1 + sql MultiTenant.with(account1) do project1 = Project.create! name: 'Project 1' projectcategory = ProjectCategory.create! name: 'project cat 1', project: project1, category: category1 - expect(Project.eager_load(:categories).to_sql).to eq(expected_sql.strip) + expect(Project.eager_load(:categories).to_sql).to eq(option1).or(eq(option2)) project = Project.eager_load(:categories).first expect(project.categories).to include(category1) @@ -451,21 +455,18 @@ category1 = Category.create! name: 'Category 1' MultiTenant.with(account1) do - expected_sql = if uses_prepared_statements? && (ActiveRecord::VERSION::MAJOR == 5 || (ActiveRecord::VERSION::MAJOR == 6 && ActiveRecord::VERSION::MINOR >= 1) || ActiveRecord::VERSION::MAJOR == 7) - <<-sql - SELECT "tasks".* FROM "tasks" INNER JOIN "projects" ON "projects"."id" = "tasks"."project_id" AND "projects"."account_id" = 1 LEFT JOIN project_categories pc ON project.category_id = pc.id WHERE "tasks"."account_id" = 1 - sql - else - <<-sql - SELECT "tasks".* FROM "tasks" INNER JOIN "projects" ON "projects"."account_id" = 1 AND "projects"."id" = "tasks"."project_id" LEFT JOIN project_categories pc ON project.category_id = pc.id WHERE "tasks"."account_id" = 1 - sql - end + option1 = <<-sql.strip + SELECT "tasks".* FROM "tasks" INNER JOIN "projects" ON "projects"."id" = "tasks"."project_id" AND "projects"."account_id" = 1 LEFT JOIN project_categories pc ON project.category_id = pc.id WHERE "tasks"."account_id" = 1 + sql + option2 = <<-sql.strip + SELECT "tasks".* FROM "tasks" INNER JOIN "projects" ON "projects"."account_id" = 1 AND "projects"."id" = "tasks"."project_id" LEFT JOIN project_categories pc ON project.category_id = pc.id WHERE "tasks"."account_id" = 1 + sql project1 = Project.create! name: 'Project 1' projectcategory = ProjectCategory.create! name: 'project cat 1', project: project1, category: category1 project1.tasks.create! name: 'baz' - expect(Task.joins(:project).joins('LEFT JOIN project_categories pc ON project.category_id = pc.id').to_sql).to eq(expected_sql.strip) + expect(Task.joins(:project).joins('LEFT JOIN project_categories pc ON project.category_id = pc.id').to_sql).to eq(option1).or(eq(option2)) end MultiTenant.without do @@ -485,32 +486,29 @@ project2 = Project.create! name: 'Project 2', account: Account.create!(name: 'Account2') MultiTenant.with(account) do - expected_sql = if uses_prepared_statements? && ActiveRecord::VERSION::MAJOR > 5 - <<-sql.strip - SELECT "projects".* FROM "projects" WHERE "projects"."account_id" = #{account.id} AND "projects"."id" = $1 LIMIT $2 - sql - else - <<-sql.strip - SELECT "projects".* FROM "projects" WHERE "projects"."account_id" = #{account.id} AND "projects"."id" = $1 LIMIT $2 - sql - end - - expect(Project).to receive(:find_by_sql).with(expected_sql, any_args).and_call_original + option1 = <<-sql.strip + SELECT "projects".* FROM "projects" WHERE "projects"."account_id" = #{account.id} AND "projects"."id" = $1 LIMIT $2 + sql + option2 = <<-sql.strip + SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 AND "projects"."account_id" = #{account.id} LIMIT $2 + sql + option3 = <<-sql.strip + SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 AND "projects"."account_id" = #{account.id} LIMIT $2 + sql + + expect(Project).to receive(:find_by_sql).with(eq(option1).or(eq(option2)).or(eq(option3)), any_args).and_call_original expect(Project.find(project.id)).to eq(project) end MultiTenant.without do - expected_sql = if uses_prepared_statements? && ActiveRecord::VERSION::MAJOR > 5 - <<-sql.strip - SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2 - sql - else - <<-sql.strip - SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2 - sql - end - - expect(Project).to receive(:find_by_sql).with(expected_sql, any_args).and_call_original + option1 = <<-sql.strip + SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2 + sql + option2 = <<-sql.strip + SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2 + sql + + expect(Project).to receive(:find_by_sql).with(eq(option1).or(eq(option2)), any_args).and_call_original expect(Project.find(project2.id)).to eq(project2) end end diff --git a/spec/activerecord-multi-tenant/query_rewriter_spec.rb b/spec/activerecord-multi-tenant/query_rewriter_spec.rb index 19984770..5d5418a8 100644 --- a/spec/activerecord-multi-tenant/query_rewriter_spec.rb +++ b/spec/activerecord-multi-tenant/query_rewriter_spec.rb @@ -98,4 +98,21 @@ }.not_to raise_error end end + + context "when joining with a model with a default scope" do + let!(:account) { Account.create!(name: "Test Account") } + + it "fetches only records within the default scope" do + alive = Domain.create(name: "alive", account: account) + deleted = Domain.create(name: "deleted", deleted: true, account: account) + page_in_alive_domain = Page.create(name: "alive", account: account, domain: alive) + page_in_deleted_domain = Page.create(name: "deleted", account: account, domain: deleted) + + expect( + MultiTenant.with(account) do + Page.joins(:domain).pluck(:id) + end + ).to eq([page_in_alive_domain.id]) + end + end end diff --git a/spec/schema.rb b/spec/schema.rb index 86340503..88d80d80 100644 --- a/spec/schema.rb +++ b/spec/schema.rb @@ -89,10 +89,21 @@ t.column :category_id, :integer end - create_table :allowed_places, force: true, id: false do |t| - t.string :account_id, :integer - t.string :name, :string + t.string :account_id, :integer + t.string :name, :string + end + + create_table :domains, force: true, partition_key: :account_id do |t| + t.column :account_id, :integer + t.column :name, :string + t.column :deleted, :boolean, default: false + end + + create_table :pages, force: true, partition_key: :account_id do |t| + t.column :account_id, :integer + t.column :name, :string + t.column :domain_id, :integer end create_distributed_table :accounts, :id @@ -108,6 +119,8 @@ create_distributed_table :uuid_records, :organization_id create_distributed_table :project_categories, :account_id create_distributed_table :allowed_places, :account_id + create_distributed_table :domains, :account_id + create_distributed_table :pages, :account_id create_reference_table :categories end @@ -204,7 +217,17 @@ class ProjectCategory < ActiveRecord::Base belongs_to :account end - class AllowedPlace < ActiveRecord::Base multi_tenant :account end + +class Domain < ActiveRecord::Base + multi_tenant :account + has_many :pages + default_scope { where(deleted: false) } +end + +class Page < ActiveRecord::Base + multi_tenant :account + belongs_to :domain +end From c968eb7204dbec6c2aee0735f7f478d953a2db5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Thu, 24 Feb 2022 14:56:11 +0000 Subject: [PATCH 009/105] Remove Travis CI --- .travis.yml | 46 ---------------------------------------------- 1 file changed, 46 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b564eb84..00000000 --- a/.travis.yml +++ /dev/null @@ -1,46 +0,0 @@ -sudo: required -cache: bundler - -language: ruby - -rvm: - - 2.5.9 - - 2.6.9 - - 2.7.5 - -gemfile: - - gemfiles/rails_5.2.gemfile - - gemfiles/rails_6.0.gemfile - - gemfiles/rails_6.1.gemfile - - gemfiles/rails_7.0.gemfile - - gemfiles/active_record_5.2.gemfile - - gemfiles/active_record_6.0.gemfile - - gemfiles/active_record_6.1.gemfile - - gemfiles/active_record_7.0.gemfile - -env: - - PREPARED_STATEMENTS=0 - - PREPARED_STATEMENTS=1 - -matrix: - fast_finish: true - exclude: - # Rails7 supports only >= 2.7.0 - - rvm: 2.5.9 - gemfile: gemfiles/rails_7.0.gemfile - - rvm: 2.5.9 - gemfile: gemfiles/active_record_7.0.gemfile - - rvm: 2.6.9 - gemfile: gemfiles/rails_7.0.gemfile - - rvm: 2.6.9 - gemfile: gemfiles/active_record_7.0.gemfile - -services: - - docker - -before_install: - - docker-compose up -d - - gem install bundler -v 2.1.4 - -script: - - bundle exec rake spec From ec434e4b378d16f057c1702df54effcd1e10f2c1 Mon Sep 17 00:00:00 2001 From: Jody Rodd Date: Tue, 26 Oct 2021 09:35:37 -0500 Subject: [PATCH 010/105] Update for Rails 5+ removal of type_cast_for_database Updates the CopyFromClient functionality to support the removal of `type_cast_for_database` from Rails 5.2+ Issue: https://github.com/citusdata/activerecord-multi-tenant/issues/122 Gets the column types and casts the data. --- lib/activerecord-multi-tenant/copy_from_client.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/activerecord-multi-tenant/copy_from_client.rb b/lib/activerecord-multi-tenant/copy_from_client.rb index e6910790..eaa468bb 100644 --- a/lib/activerecord-multi-tenant/copy_from_client.rb +++ b/lib/activerecord-multi-tenant/copy_from_client.rb @@ -9,7 +9,7 @@ def initialize(conn, column_types) end def <<(row) - row = row.map.with_index { |val, idx| @column_types[idx].type_cast_for_database(val) } + row = row.map.with_index { |val, idx| @column_types[idx].serialize(val) } @conn.put_copy_data(row) @count += 1 end @@ -18,7 +18,7 @@ def <<(row) module CopyFromClient def copy_from_client(columns, &block) conn = connection.raw_connection - column_types = columns.map { |c| columns_hash[c.to_s] } + column_types = columns.map { |c| type_for_attribute(c.to_s) } helper = MultiTenant::CopyFromClientHelper.new(conn, column_types) conn.copy_data %{COPY #{quoted_table_name}("#{columns.join('","')}") FROM STDIN}, PG::TextEncoder::CopyRow.new do block.call helper From bca020ab0af805c5acd17a02fa7d5792fde0a503 Mon Sep 17 00:00:00 2001 From: Matt Larraz Date: Thu, 24 Feb 2022 18:49:15 -0500 Subject: [PATCH 011/105] Test Rails 7 and Ruby 3.0/3.1 --- .github/workflows/CI.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 99b32503..6f97196e 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -16,13 +16,17 @@ jobs: - '2.5' - '2.6' - '2.7' + - '3.0' + - '3.1' gemfile: - rails_5.2 - rails_6.0 - rails_6.1 + - rails_7.0 - active_record_5.2 - active_record_6.0 - active_record_6.1 + - active_record_7.0 prepared_statements: [true, false] name: Ruby ${{ matrix.ruby }} / ${{ matrix.gemfile }} ${{ (matrix.prepared_statements && 'w/ prepared statements') || '' }} env: From 2b53373b618918cce8d246938dc089cef593aad7 Mon Sep 17 00:00:00 2001 From: Matt Larraz Date: Thu, 24 Feb 2022 18:58:20 -0500 Subject: [PATCH 012/105] Fix lockfiles --- gemfiles/active_record_7.0.gemfile.lock | 4 +++- gemfiles/rails_7.0.gemfile.lock | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/gemfiles/active_record_7.0.gemfile.lock b/gemfiles/active_record_7.0.gemfile.lock index d0e12e45..7c89dd24 100644 --- a/gemfiles/active_record_7.0.gemfile.lock +++ b/gemfiles/active_record_7.0.gemfile.lock @@ -91,9 +91,11 @@ GEM marcel (1.0.2) method_source (1.0.0) mini_mime (1.1.2) + mini_portile2 (2.8.0) minitest (5.15.0) nio4r (2.5.8) - nokogiri (1.12.5-x86_64-darwin) + nokogiri (1.12.5) + mini_portile2 (~> 2.8.0) racc (~> 1.4) pg (1.2.3) pry (0.13.1) diff --git a/gemfiles/rails_7.0.gemfile.lock b/gemfiles/rails_7.0.gemfile.lock index cf86a620..f939e66e 100644 --- a/gemfiles/rails_7.0.gemfile.lock +++ b/gemfiles/rails_7.0.gemfile.lock @@ -91,9 +91,11 @@ GEM marcel (1.0.2) method_source (1.0.0) mini_mime (1.1.2) + mini_portile2 (2.8.0) minitest (5.15.0) nio4r (2.5.8) - nokogiri (1.12.5-x86_64-darwin) + nokogiri (1.12.5) + mini_portile2 (~> 2.8.0) racc (~> 1.4) pg (1.2.3) pry (0.13.1) From c45d3e306911bcd44a34e5be70b1f775b13eb0c7 Mon Sep 17 00:00:00 2001 From: Eren Basak Date: Wed, 2 Mar 2022 21:01:58 +0300 Subject: [PATCH 013/105] workaround .and_call_original issue --- .../model_extensions_spec.rb | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/spec/activerecord-multi-tenant/model_extensions_spec.rb b/spec/activerecord-multi-tenant/model_extensions_spec.rb index 63db21e2..65f2e208 100644 --- a/spec/activerecord-multi-tenant/model_extensions_spec.rb +++ b/spec/activerecord-multi-tenant/model_extensions_spec.rb @@ -496,7 +496,12 @@ SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 AND "projects"."account_id" = #{account.id} LIMIT $2 sql - expect(Project).to receive(:find_by_sql).with(eq(option1).or(eq(option2)).or(eq(option3)), any_args).and_call_original + # Couldn't make the following line pass for some reason, so came up with an uglier alternative + # expect(Project).to receive(:find_by_sql).with(eq(option1).or(eq(option2)).or(eq(option3)), any_args).and_call_original + expect(Project).to receive(:find_by_sql).and_wrap_original do |m, *args| + expect(args[0]).to(eq(option1).or(eq(option2)).or(eq(option3))) + m.call(args[0], args[1], preparable:args[2][:preparable]) + end expect(Project.find(project.id)).to eq(project) end @@ -508,7 +513,12 @@ SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2 sql - expect(Project).to receive(:find_by_sql).with(eq(option1).or(eq(option2)), any_args).and_call_original + # Couldn't make the following line pass for some reason, so came up with an uglier alternative + # expect(Project).to receive(:find_by_sql).with(eq(option1).or(eq(option2)), any_args).and_call_original + expect(Project).to receive(:find_by_sql).and_wrap_original do |m, *args| + expect(args[0]).to(eq(option1).or(eq(option2))) + m.call(args[0], args[1], preparable:args[2][:preparable]) + end expect(Project.find(project2.id)).to eq(project2) end end From 88a1439c7b5af456288ac29d3ec69f5f905c26a4 Mon Sep 17 00:00:00 2001 From: Eren Basak Date: Thu, 3 Mar 2022 19:34:54 +0300 Subject: [PATCH 014/105] Exclude incompatible ruby/rails pairs from CI --- .github/workflows/CI.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 6f97196e..c280bd3c 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -28,6 +28,30 @@ jobs: - active_record_6.1 - active_record_7.0 prepared_statements: [true, false] + exclude: + # activesupport-7.0.0 requires ruby version >= 2.7.0 + - ruby: '2.5' + gemfile: 'rails_7.0' + - ruby: '2.5' + gemfile: 'active_record_7.0' + - ruby: '2.6' + gemfile: 'rails_7.0' + - ruby: '2.6' + gemfile: 'active_record_7.0' + # ruby >3 and activesupport 5.2 are not compatible + - ruby: '3.0' + gemfile: 'rails_5.2' + - ruby: '3.0' + gemfile: 'active_record_5.2' + - ruby: '3.1' + gemfile: 'rails_5.2' + - ruby: '3.1' + gemfile: 'active_record_5.2' + # TEMP: nokogiri 1.12.5, which is a dependency of rails/active_record 7, is not compatible with ruby 3.1 + - ruby: '3.1' + gemfile: 'rails_7.0' + - ruby: '3.1' + gemfile: 'active_record_7.0' name: Ruby ${{ matrix.ruby }} / ${{ matrix.gemfile }} ${{ (matrix.prepared_statements && 'w/ prepared statements') || '' }} env: BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile From 5823ed806ab69415a62f029a1dbf595639d6f321 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Mar 2022 21:30:41 +0000 Subject: [PATCH 015/105] Bump activestorage from 6.0.3.3 to 6.0.4.7 Bumps [activestorage](https://github.com/rails/rails) from 6.0.3.3 to 6.0.4.7. - [Release notes](https://github.com/rails/rails/releases) - [Changelog](https://github.com/rails/rails/blob/v7.0.2.3/activestorage/CHANGELOG.md) - [Commits](https://github.com/rails/rails/compare/v6.0.3.3...v6.0.4.7) --- updated-dependencies: - dependency-name: activestorage dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 142 +++++++++++++++++++++++++-------------------------- 1 file changed, 69 insertions(+), 73 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 7f49e62e..674b59cb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,56 +8,56 @@ PATH GEM remote: https://rubygems.org/ specs: - actioncable (6.0.3.3) - actionpack (= 6.0.3.3) + actioncable (6.0.4.7) + actionpack (= 6.0.4.7) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.0.3.3) - actionpack (= 6.0.3.3) - activejob (= 6.0.3.3) - activerecord (= 6.0.3.3) - activestorage (= 6.0.3.3) - activesupport (= 6.0.3.3) + actionmailbox (6.0.4.7) + actionpack (= 6.0.4.7) + activejob (= 6.0.4.7) + activerecord (= 6.0.4.7) + activestorage (= 6.0.4.7) + activesupport (= 6.0.4.7) mail (>= 2.7.1) - actionmailer (6.0.3.3) - actionpack (= 6.0.3.3) - actionview (= 6.0.3.3) - activejob (= 6.0.3.3) + actionmailer (6.0.4.7) + actionpack (= 6.0.4.7) + actionview (= 6.0.4.7) + activejob (= 6.0.4.7) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.0.3.3) - actionview (= 6.0.3.3) - activesupport (= 6.0.3.3) + actionpack (6.0.4.7) + actionview (= 6.0.4.7) + activesupport (= 6.0.4.7) rack (~> 2.0, >= 2.0.8) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.0.3.3) - actionpack (= 6.0.3.3) - activerecord (= 6.0.3.3) - activestorage (= 6.0.3.3) - activesupport (= 6.0.3.3) + actiontext (6.0.4.7) + actionpack (= 6.0.4.7) + activerecord (= 6.0.4.7) + activestorage (= 6.0.4.7) + activesupport (= 6.0.4.7) nokogiri (>= 1.8.5) - actionview (6.0.3.3) - activesupport (= 6.0.3.3) + actionview (6.0.4.7) + activesupport (= 6.0.4.7) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.0.3.3) - activesupport (= 6.0.3.3) + activejob (6.0.4.7) + activesupport (= 6.0.4.7) globalid (>= 0.3.6) - activemodel (6.0.3.3) - activesupport (= 6.0.3.3) - activerecord (6.0.3.3) - activemodel (= 6.0.3.3) - activesupport (= 6.0.3.3) - activestorage (6.0.3.3) - actionpack (= 6.0.3.3) - activejob (= 6.0.3.3) - activerecord (= 6.0.3.3) - marcel (~> 0.3.1) - activesupport (6.0.3.3) + activemodel (6.0.4.7) + activesupport (= 6.0.4.7) + activerecord (6.0.4.7) + activemodel (= 6.0.4.7) + activesupport (= 6.0.4.7) + activestorage (6.0.4.7) + actionpack (= 6.0.4.7) + activejob (= 6.0.4.7) + activerecord (= 6.0.4.7) + marcel (~> 1.0.0) + activesupport (6.0.4.7) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) @@ -70,30 +70,26 @@ GEM builder (3.2.4) byebug (11.0.1) coderay (1.1.2) - concurrent-ruby (1.1.7) + concurrent-ruby (1.1.9) connection_pool (2.2.2) crass (1.0.6) diff-lcs (1.3) - erubi (1.9.0) - globalid (0.4.2) - activesupport (>= 4.2.0) - i18n (1.8.5) + erubi (1.10.0) + globalid (1.0.0) + activesupport (>= 5.0) + i18n (1.10.0) concurrent-ruby (~> 1.0) - loofah (2.7.0) + loofah (2.14.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) mini_mime (>= 0.1.1) - marcel (0.3.3) - mimemagic (~> 0.3.2) + marcel (1.0.2) method_source (0.9.2) - mimemagic (0.3.10) - nokogiri (~> 1) - rake - mini_mime (1.0.2) + mini_mime (1.1.2) mini_portile2 (2.8.0) - minitest (5.14.4) - nio4r (2.5.4) + minitest (5.15.0) + nio4r (2.5.8) nokogiri (1.13.3) mini_portile2 (~> 2.8.0) racc (~> 1.4) @@ -110,29 +106,29 @@ GEM rack rack-test (1.1.0) rack (>= 1.0, < 3) - rails (6.0.3.3) - actioncable (= 6.0.3.3) - actionmailbox (= 6.0.3.3) - actionmailer (= 6.0.3.3) - actionpack (= 6.0.3.3) - actiontext (= 6.0.3.3) - actionview (= 6.0.3.3) - activejob (= 6.0.3.3) - activemodel (= 6.0.3.3) - activerecord (= 6.0.3.3) - activestorage (= 6.0.3.3) - activesupport (= 6.0.3.3) + rails (6.0.4.7) + actioncable (= 6.0.4.7) + actionmailbox (= 6.0.4.7) + actionmailer (= 6.0.4.7) + actionpack (= 6.0.4.7) + actiontext (= 6.0.4.7) + actionview (= 6.0.4.7) + activejob (= 6.0.4.7) + activemodel (= 6.0.4.7) + activerecord (= 6.0.4.7) + activestorage (= 6.0.4.7) + activesupport (= 6.0.4.7) bundler (>= 1.3.0) - railties (= 6.0.3.3) + railties (= 6.0.4.7) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.3.0) + rails-html-sanitizer (1.4.2) loofah (~> 2.3) - railties (6.0.3.3) - actionpack (= 6.0.3.3) - activesupport (= 6.0.3.3) + railties (6.0.4.7) + actionpack (= 6.0.4.7) + activesupport (= 6.0.4.7) method_source rake (>= 0.8.7) thor (>= 0.20.3, < 2.0) @@ -166,21 +162,21 @@ GEM rack (>= 1.5.0) rack-protection (>= 1.5.0) redis (>= 3.3.5, < 5) - sprockets (4.0.2) + sprockets (4.0.3) concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (3.2.2) - actionpack (>= 4.0) - activesupport (>= 4.0) + sprockets-rails (3.4.2) + actionpack (>= 5.2) + activesupport (>= 5.2) sprockets (>= 3.0.0) thor (0.20.3) thread_safe (0.3.6) - tzinfo (1.2.7) + tzinfo (1.2.9) thread_safe (~> 0.1) - websocket-driver (0.7.3) + websocket-driver (0.7.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) - zeitwerk (2.4.0) + zeitwerk (2.5.4) PLATFORMS ruby From 0eb509c0d0fd45864b3ff1358f7c42b8b5233f6f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Mar 2022 16:25:17 +0000 Subject: [PATCH 016/105] Bump sidekiq from 5.2.7 to 6.4.1 Bumps [sidekiq](https://github.com/mperham/sidekiq) from 5.2.7 to 6.4.1. - [Release notes](https://github.com/mperham/sidekiq/releases) - [Changelog](https://github.com/mperham/sidekiq/blob/main/Changes.md) - [Commits](https://github.com/mperham/sidekiq/compare/v5.2.7...v6.4.1) --- updated-dependencies: - dependency-name: sidekiq dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 674b59cb..f908f235 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -71,7 +71,7 @@ GEM byebug (11.0.1) coderay (1.1.2) concurrent-ruby (1.1.9) - connection_pool (2.2.2) + connection_pool (2.2.5) crass (1.0.6) diff-lcs (1.3) erubi (1.10.0) @@ -102,8 +102,6 @@ GEM pry (~> 0.10) racc (1.6.0) rack (2.2.3) - rack-protection (2.0.5) - rack rack-test (1.1.0) rack (>= 1.0, < 3) rails (6.0.4.7) @@ -133,7 +131,7 @@ GEM rake (>= 0.8.7) thor (>= 0.20.3, < 2.0) rake (13.0.1) - redis (4.1.2) + redis (4.6.0) request_store (1.5.0) rack (>= 1.4) rspec (3.8.0) @@ -157,11 +155,10 @@ GEM rspec-mocks (~> 3.8.0) rspec-support (~> 3.8.0) rspec-support (3.8.2) - sidekiq (5.2.7) - connection_pool (~> 2.2, >= 2.2.2) - rack (>= 1.5.0) - rack-protection (>= 1.5.0) - redis (>= 3.3.5, < 5) + sidekiq (6.4.1) + connection_pool (>= 2.2.2) + rack (~> 2.0) + redis (>= 4.2.0) sprockets (4.0.3) concurrent-ruby (~> 1.0) rack (> 1, < 3) From 19e0d5da8c1cb021db9d1deb1e5e61da01f26238 Mon Sep 17 00:00:00 2001 From: Matt Larraz Date: Thu, 11 Feb 2021 14:37:29 -0500 Subject: [PATCH 017/105] Add lockfiles to gitignore These don't really make sense for a library, as they are testing just one out of many possible manifest configurations. This has the inadvertent effect of fixing the build, as it would currently be broken due to https://github.com/mimemagicrb/mimemagic/issues/97 which led to a dependency being yanked from RubyGems --- .github/workflows/CI.yml | 5 - .gitignore | 2 + Gemfile.lock | 194 ----------------------- gemfiles/active_record_5.2.gemfile.lock | 190 ---------------------- gemfiles/active_record_6.0.gemfile.lock | 202 ------------------------ gemfiles/active_record_6.1.gemfile.lock | 202 ------------------------ gemfiles/active_record_7.0.gemfile.lock | 191 ---------------------- gemfiles/rails_5.2.gemfile.lock | 190 ---------------------- gemfiles/rails_6.0.gemfile.lock | 202 ------------------------ gemfiles/rails_6.1.gemfile.lock | 202 ------------------------ gemfiles/rails_7.0.gemfile.lock | 191 ---------------------- 11 files changed, 2 insertions(+), 1769 deletions(-) delete mode 100644 Gemfile.lock delete mode 100644 gemfiles/active_record_5.2.gemfile.lock delete mode 100644 gemfiles/active_record_6.0.gemfile.lock delete mode 100644 gemfiles/active_record_6.1.gemfile.lock delete mode 100644 gemfiles/active_record_7.0.gemfile.lock delete mode 100644 gemfiles/rails_5.2.gemfile.lock delete mode 100644 gemfiles/rails_6.0.gemfile.lock delete mode 100644 gemfiles/rails_6.1.gemfile.lock delete mode 100644 gemfiles/rails_7.0.gemfile.lock diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index c280bd3c..d22b6b67 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -47,11 +47,6 @@ jobs: gemfile: 'rails_5.2' - ruby: '3.1' gemfile: 'active_record_5.2' - # TEMP: nokogiri 1.12.5, which is a dependency of rails/active_record 7, is not compatible with ruby 3.1 - - ruby: '3.1' - gemfile: 'rails_7.0' - - ruby: '3.1' - gemfile: 'active_record_7.0' name: Ruby ${{ matrix.ruby }} / ${{ matrix.gemfile }} ${{ (matrix.prepared_statements && 'w/ prepared statements') || '' }} env: BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile diff --git a/.gitignore b/.gitignore index d8f08963..a14359c7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ spec/debug.log pkg/ *.rb# *.*~ +Gemfile.lock +*.gemfile.lock diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index f908f235..00000000 --- a/Gemfile.lock +++ /dev/null @@ -1,194 +0,0 @@ -PATH - remote: . - specs: - activerecord-multi-tenant (1.1.1) - rails (>= 4.2) - request_store (>= 1.0.5) - -GEM - remote: https://rubygems.org/ - specs: - actioncable (6.0.4.7) - actionpack (= 6.0.4.7) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - actionmailbox (6.0.4.7) - actionpack (= 6.0.4.7) - activejob (= 6.0.4.7) - activerecord (= 6.0.4.7) - activestorage (= 6.0.4.7) - activesupport (= 6.0.4.7) - mail (>= 2.7.1) - actionmailer (6.0.4.7) - actionpack (= 6.0.4.7) - actionview (= 6.0.4.7) - activejob (= 6.0.4.7) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (6.0.4.7) - actionview (= 6.0.4.7) - activesupport (= 6.0.4.7) - rack (~> 2.0, >= 2.0.8) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.0.4.7) - actionpack (= 6.0.4.7) - activerecord (= 6.0.4.7) - activestorage (= 6.0.4.7) - activesupport (= 6.0.4.7) - nokogiri (>= 1.8.5) - actionview (6.0.4.7) - activesupport (= 6.0.4.7) - builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.0.4.7) - activesupport (= 6.0.4.7) - globalid (>= 0.3.6) - activemodel (6.0.4.7) - activesupport (= 6.0.4.7) - activerecord (6.0.4.7) - activemodel (= 6.0.4.7) - activesupport (= 6.0.4.7) - activestorage (6.0.4.7) - actionpack (= 6.0.4.7) - activejob (= 6.0.4.7) - activerecord (= 6.0.4.7) - marcel (~> 1.0.0) - activesupport (6.0.4.7) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - zeitwerk (~> 2.2, >= 2.2.2) - appraisal (2.2.0) - bundler - rake - thor (>= 0.14.0) - builder (3.2.4) - byebug (11.0.1) - coderay (1.1.2) - concurrent-ruby (1.1.9) - connection_pool (2.2.5) - crass (1.0.6) - diff-lcs (1.3) - erubi (1.10.0) - globalid (1.0.0) - activesupport (>= 5.0) - i18n (1.10.0) - concurrent-ruby (~> 1.0) - loofah (2.14.0) - crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) - mini_mime (>= 0.1.1) - marcel (1.0.2) - method_source (0.9.2) - mini_mime (1.1.2) - mini_portile2 (2.8.0) - minitest (5.15.0) - nio4r (2.5.8) - nokogiri (1.13.3) - mini_portile2 (~> 2.8.0) - racc (~> 1.4) - pg (1.1.4) - pry (0.12.2) - coderay (~> 1.1.0) - method_source (~> 0.9.0) - pry-byebug (3.7.0) - byebug (~> 11.0) - pry (~> 0.10) - racc (1.6.0) - rack (2.2.3) - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (6.0.4.7) - actioncable (= 6.0.4.7) - actionmailbox (= 6.0.4.7) - actionmailer (= 6.0.4.7) - actionpack (= 6.0.4.7) - actiontext (= 6.0.4.7) - actionview (= 6.0.4.7) - activejob (= 6.0.4.7) - activemodel (= 6.0.4.7) - activerecord (= 6.0.4.7) - activestorage (= 6.0.4.7) - activesupport (= 6.0.4.7) - bundler (>= 1.3.0) - railties (= 6.0.4.7) - sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) - nokogiri (>= 1.6) - rails-html-sanitizer (1.4.2) - loofah (~> 2.3) - railties (6.0.4.7) - actionpack (= 6.0.4.7) - activesupport (= 6.0.4.7) - method_source - rake (>= 0.8.7) - thor (>= 0.20.3, < 2.0) - rake (13.0.1) - redis (4.6.0) - request_store (1.5.0) - rack (>= 1.4) - rspec (3.8.0) - rspec-core (~> 3.8.0) - rspec-expectations (~> 3.8.0) - rspec-mocks (~> 3.8.0) - rspec-core (3.8.2) - rspec-support (~> 3.8.0) - rspec-expectations (3.8.4) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.8.0) - rspec-mocks (3.8.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.8.0) - rspec-rails (3.8.2) - actionpack (>= 3.0) - activesupport (>= 3.0) - railties (>= 3.0) - rspec-core (~> 3.8.0) - rspec-expectations (~> 3.8.0) - rspec-mocks (~> 3.8.0) - rspec-support (~> 3.8.0) - rspec-support (3.8.2) - sidekiq (6.4.1) - connection_pool (>= 2.2.2) - rack (~> 2.0) - redis (>= 4.2.0) - sprockets (4.0.3) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.4.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - sprockets (>= 3.0.0) - thor (0.20.3) - thread_safe (0.3.6) - tzinfo (1.2.9) - thread_safe (~> 0.1) - websocket-driver (0.7.5) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.5) - zeitwerk (2.5.4) - -PLATFORMS - ruby - -DEPENDENCIES - activerecord-multi-tenant! - appraisal - pg - pry - pry-byebug - rake - rspec (>= 3.0) - rspec-rails - sidekiq - thor - -BUNDLED WITH - 2.2.22 diff --git a/gemfiles/active_record_5.2.gemfile.lock b/gemfiles/active_record_5.2.gemfile.lock deleted file mode 100644 index 18b346dc..00000000 --- a/gemfiles/active_record_5.2.gemfile.lock +++ /dev/null @@ -1,190 +0,0 @@ -PATH - remote: .. - specs: - activerecord-multi-tenant (1.1.1) - rails (>= 4.2) - request_store (>= 1.0.5) - -GEM - remote: https://rubygems.org/ - specs: - actioncable (5.2.3) - actionpack (= 5.2.3) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - actionmailer (5.2.3) - actionpack (= 5.2.3) - actionview (= 5.2.3) - activejob (= 5.2.3) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (5.2.3) - actionview (= 5.2.3) - activesupport (= 5.2.3) - rack (~> 2.0) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.3) - activesupport (= 5.2.3) - builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (5.2.3) - activesupport (= 5.2.3) - globalid (>= 0.3.6) - activemodel (5.2.3) - activesupport (= 5.2.3) - activerecord (5.2.3) - activemodel (= 5.2.3) - activesupport (= 5.2.3) - arel (>= 9.0) - activestorage (5.2.3) - actionpack (= 5.2.3) - activerecord (= 5.2.3) - marcel (~> 0.3.1) - activesupport (5.2.3) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - appraisal (2.2.0) - bundler - rake - thor (>= 0.14.0) - arel (9.0.0) - builder (3.2.4) - byebug (11.1.3) - coderay (1.1.2) - concurrent-ruby (1.1.6) - connection_pool (2.2.2) - crass (1.0.6) - diff-lcs (1.3) - erubi (1.9.0) - globalid (0.4.2) - activesupport (>= 4.2.0) - i18n (0.9.5) - concurrent-ruby (~> 1.0) - loofah (2.5.0) - crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) - mini_mime (>= 0.1.1) - marcel (0.3.3) - mimemagic (~> 0.3.2) - method_source (1.0.0) - mimemagic (0.3.10) - nokogiri (~> 1) - rake - mini_mime (1.0.2) - mini_portile2 (2.1.0) - minitest (5.14.4) - nio4r (2.3.1) - nokogiri (1.7.2) - mini_portile2 (~> 2.1.0) - pg (1.2.3) - pry (0.13.1) - coderay (~> 1.1) - method_source (~> 1.0) - pry-byebug (3.9.0) - byebug (~> 11.0) - pry (~> 0.13.0) - rack (2.2.2) - rack-protection (2.0.8.1) - rack - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (5.2.3) - actioncable (= 5.2.3) - actionmailer (= 5.2.3) - actionpack (= 5.2.3) - actionview (= 5.2.3) - activejob (= 5.2.3) - activemodel (= 5.2.3) - activerecord (= 5.2.3) - activestorage (= 5.2.3) - activesupport (= 5.2.3) - bundler (>= 1.3.0) - railties (= 5.2.3) - sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) - nokogiri (>= 1.6) - rails-html-sanitizer (1.3.0) - loofah (~> 2.3) - railties (5.2.3) - actionpack (= 5.2.3) - activesupport (= 5.2.3) - method_source - rake (>= 0.8.7) - thor (>= 0.19.0, < 2.0) - rake (12.0.0) - redis (3.3.3) - request_store (1.5.0) - rack (>= 1.4) - rspec (3.9.0) - rspec-core (~> 3.9.0) - rspec-expectations (~> 3.9.0) - rspec-mocks (~> 3.9.0) - rspec-core (3.9.2) - rspec-support (~> 3.9.3) - rspec-expectations (3.9.2) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-mocks (3.9.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-rails (4.0.1) - actionpack (>= 4.2) - activesupport (>= 4.2) - railties (>= 4.2) - rspec-core (~> 3.9) - rspec-expectations (~> 3.9) - rspec-mocks (~> 3.9) - rspec-support (~> 3.9) - rspec-support (3.9.3) - sidekiq (5.0.4) - concurrent-ruby (~> 1.0) - connection_pool (~> 2.2, >= 2.2.0) - rack-protection (>= 1.5.0) - redis (~> 3.3, >= 3.3.3) - sprockets (3.7.2) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.2.2) - actionpack (>= 4.0) - activesupport (>= 4.0) - sprockets (>= 3.0.0) - thor (1.0.1) - thread_safe (0.3.6) - tzinfo (1.2.7) - thread_safe (~> 0.1) - websocket-driver (0.7.3) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.5) - -PLATFORMS - ruby - -DEPENDENCIES - activerecord (~> 5.2.0) - activerecord-multi-tenant! - appraisal - byebug (~> 11.0) - i18n (~> 0.9.5) - nio4r (~> 2.3.1) - nokogiri (~> 1.7.1) - pg - pry - pry-byebug (= 3.9.0) - rake (= 12.0.0) - redis (= 3.3.3) - rspec (>= 3.0) - rspec-rails - sidekiq - sprockets (~> 3.7.1) - thor - -BUNDLED WITH - 2.2.22 diff --git a/gemfiles/active_record_6.0.gemfile.lock b/gemfiles/active_record_6.0.gemfile.lock deleted file mode 100644 index d5b57876..00000000 --- a/gemfiles/active_record_6.0.gemfile.lock +++ /dev/null @@ -1,202 +0,0 @@ -PATH - remote: .. - specs: - activerecord-multi-tenant (1.1.1) - rails (>= 4.2) - request_store (>= 1.0.5) - -GEM - remote: https://rubygems.org/ - specs: - actioncable (6.0.3.1) - actionpack (= 6.0.3.1) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - actionmailbox (6.0.3.1) - actionpack (= 6.0.3.1) - activejob (= 6.0.3.1) - activerecord (= 6.0.3.1) - activestorage (= 6.0.3.1) - activesupport (= 6.0.3.1) - mail (>= 2.7.1) - actionmailer (6.0.3.1) - actionpack (= 6.0.3.1) - actionview (= 6.0.3.1) - activejob (= 6.0.3.1) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (6.0.3.1) - actionview (= 6.0.3.1) - activesupport (= 6.0.3.1) - rack (~> 2.0, >= 2.0.8) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.0.3.1) - actionpack (= 6.0.3.1) - activerecord (= 6.0.3.1) - activestorage (= 6.0.3.1) - activesupport (= 6.0.3.1) - nokogiri (>= 1.8.5) - actionview (6.0.3.1) - activesupport (= 6.0.3.1) - builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.0.3.1) - activesupport (= 6.0.3.1) - globalid (>= 0.3.6) - activemodel (6.0.3.1) - activesupport (= 6.0.3.1) - activerecord (6.0.3.1) - activemodel (= 6.0.3.1) - activesupport (= 6.0.3.1) - activestorage (6.0.3.1) - actionpack (= 6.0.3.1) - activejob (= 6.0.3.1) - activerecord (= 6.0.3.1) - marcel (~> 0.3.1) - activesupport (6.0.3.1) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - zeitwerk (~> 2.2, >= 2.2.2) - appraisal (2.2.0) - bundler - rake - thor (>= 0.14.0) - builder (3.2.4) - byebug (11.1.3) - coderay (1.1.2) - concurrent-ruby (1.1.6) - connection_pool (2.2.2) - crass (1.0.6) - diff-lcs (1.3) - erubi (1.9.0) - globalid (0.4.2) - activesupport (>= 4.2.0) - i18n (1.8.2) - concurrent-ruby (~> 1.0) - loofah (2.5.0) - crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) - mini_mime (>= 0.1.1) - marcel (0.3.3) - mimemagic (~> 0.3.2) - method_source (1.0.0) - mimemagic (0.3.10) - nokogiri (~> 1) - rake - mini_mime (1.0.2) - mini_portile2 (2.6.1) - minitest (5.14.4) - nio4r (2.5.4) - nokogiri (1.12.5) - mini_portile2 (~> 2.6.1) - racc (~> 1.4) - pg (1.2.3) - pry (0.13.1) - coderay (~> 1.1) - method_source (~> 1.0) - pry-byebug (3.9.0) - byebug (~> 11.0) - pry (~> 0.13.0) - racc (1.6.0) - rack (2.2.2) - rack-protection (2.0.8.1) - rack - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (6.0.3.1) - actioncable (= 6.0.3.1) - actionmailbox (= 6.0.3.1) - actionmailer (= 6.0.3.1) - actionpack (= 6.0.3.1) - actiontext (= 6.0.3.1) - actionview (= 6.0.3.1) - activejob (= 6.0.3.1) - activemodel (= 6.0.3.1) - activerecord (= 6.0.3.1) - activestorage (= 6.0.3.1) - activesupport (= 6.0.3.1) - bundler (>= 1.3.0) - railties (= 6.0.3.1) - sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) - nokogiri (>= 1.6) - rails-html-sanitizer (1.3.0) - loofah (~> 2.3) - railties (6.0.3.1) - actionpack (= 6.0.3.1) - activesupport (= 6.0.3.1) - method_source - rake (>= 0.8.7) - thor (>= 0.20.3, < 2.0) - rake (13.0.6) - redis (4.1.4) - request_store (1.5.0) - rack (>= 1.4) - rspec (3.9.0) - rspec-core (~> 3.9.0) - rspec-expectations (~> 3.9.0) - rspec-mocks (~> 3.9.0) - rspec-core (3.9.2) - rspec-support (~> 3.9.3) - rspec-expectations (3.9.2) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-mocks (3.9.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-rails (4.0.1) - actionpack (>= 4.2) - activesupport (>= 4.2) - railties (>= 4.2) - rspec-core (~> 3.9) - rspec-expectations (~> 3.9) - rspec-mocks (~> 3.9) - rspec-support (~> 3.9) - rspec-support (3.9.3) - sidekiq (6.0.7) - connection_pool (>= 2.2.2) - rack (~> 2.0) - rack-protection (>= 2.0.0) - redis (>= 4.1.0) - sprockets (4.0.2) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.2.2) - actionpack (>= 4.0) - activesupport (>= 4.0) - sprockets (>= 3.0.0) - thor (1.0.1) - thread_safe (0.3.6) - tzinfo (1.2.7) - thread_safe (~> 0.1) - websocket-driver (0.7.3) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.5) - zeitwerk (2.3.0) - -PLATFORMS - ruby - -DEPENDENCIES - activerecord (~> 6.0.3) - activerecord-multi-tenant! - appraisal - pg - pry - pry-byebug - rake - rspec (>= 3.0) - rspec-rails - sidekiq - thor - -BUNDLED WITH - 2.2.22 diff --git a/gemfiles/active_record_6.1.gemfile.lock b/gemfiles/active_record_6.1.gemfile.lock deleted file mode 100644 index a2ce68d8..00000000 --- a/gemfiles/active_record_6.1.gemfile.lock +++ /dev/null @@ -1,202 +0,0 @@ -PATH - remote: .. - specs: - activerecord-multi-tenant (1.1.1) - rails (>= 4.2) - request_store (>= 1.0.5) - -GEM - remote: https://rubygems.org/ - specs: - actioncable (6.1.0) - actionpack (= 6.1.0) - activesupport (= 6.1.0) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - actionmailbox (6.1.0) - actionpack (= 6.1.0) - activejob (= 6.1.0) - activerecord (= 6.1.0) - activestorage (= 6.1.0) - activesupport (= 6.1.0) - mail (>= 2.7.1) - actionmailer (6.1.0) - actionpack (= 6.1.0) - actionview (= 6.1.0) - activejob (= 6.1.0) - activesupport (= 6.1.0) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (6.1.0) - actionview (= 6.1.0) - activesupport (= 6.1.0) - rack (~> 2.0, >= 2.0.9) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.0) - actionpack (= 6.1.0) - activerecord (= 6.1.0) - activestorage (= 6.1.0) - activesupport (= 6.1.0) - nokogiri (>= 1.8.5) - actionview (6.1.0) - activesupport (= 6.1.0) - builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.1.0) - activesupport (= 6.1.0) - globalid (>= 0.3.6) - activemodel (6.1.0) - activesupport (= 6.1.0) - activerecord (6.1.0) - activemodel (= 6.1.0) - activesupport (= 6.1.0) - activestorage (6.1.0) - actionpack (= 6.1.0) - activejob (= 6.1.0) - activerecord (= 6.1.0) - activesupport (= 6.1.0) - marcel (~> 0.3.1) - mimemagic (~> 0.3.2) - activesupport (6.1.0) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 1.6, < 2) - minitest (>= 5.1) - tzinfo (~> 2.0) - zeitwerk (~> 2.3) - appraisal (2.3.0) - bundler - rake - thor (>= 0.14.0) - builder (3.2.4) - byebug (11.1.3) - coderay (1.1.3) - concurrent-ruby (1.1.7) - connection_pool (2.2.3) - crass (1.0.6) - diff-lcs (1.4.4) - erubi (1.10.0) - globalid (0.4.2) - activesupport (>= 4.2.0) - i18n (1.8.5) - concurrent-ruby (~> 1.0) - loofah (2.8.0) - crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) - mini_mime (>= 0.1.1) - marcel (0.3.3) - mimemagic (~> 0.3.2) - method_source (1.0.0) - mimemagic (0.3.10) - nokogiri (~> 1) - rake - mini_mime (1.0.2) - mini_portile2 (2.6.1) - minitest (5.14.4) - nio4r (2.5.4) - nokogiri (1.12.5) - mini_portile2 (~> 2.6.1) - racc (~> 1.4) - pg (1.2.3) - pry (0.13.1) - coderay (~> 1.1) - method_source (~> 1.0) - pry-byebug (3.9.0) - byebug (~> 11.0) - pry (~> 0.13.0) - racc (1.6.0) - rack (2.2.3) - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (6.1.0) - actioncable (= 6.1.0) - actionmailbox (= 6.1.0) - actionmailer (= 6.1.0) - actionpack (= 6.1.0) - actiontext (= 6.1.0) - actionview (= 6.1.0) - activejob (= 6.1.0) - activemodel (= 6.1.0) - activerecord (= 6.1.0) - activestorage (= 6.1.0) - activesupport (= 6.1.0) - bundler (>= 1.15.0) - railties (= 6.1.0) - sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) - nokogiri (>= 1.6) - rails-html-sanitizer (1.3.0) - loofah (~> 2.3) - railties (6.1.0) - actionpack (= 6.1.0) - activesupport (= 6.1.0) - method_source - rake (>= 0.8.7) - thor (~> 1.0) - rake (13.0.6) - redis (4.2.5) - request_store (1.5.0) - rack (>= 1.4) - rspec (3.10.0) - rspec-core (~> 3.10.0) - rspec-expectations (~> 3.10.0) - rspec-mocks (~> 3.10.0) - rspec-core (3.10.1) - rspec-support (~> 3.10.0) - rspec-expectations (3.10.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-mocks (3.10.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-rails (4.0.2) - actionpack (>= 4.2) - activesupport (>= 4.2) - railties (>= 4.2) - rspec-core (~> 3.10) - rspec-expectations (~> 3.10) - rspec-mocks (~> 3.10) - rspec-support (~> 3.10) - rspec-support (3.10.1) - sidekiq (6.1.2) - connection_pool (>= 2.2.2) - rack (~> 2.0) - redis (>= 4.2.0) - sprockets (4.0.2) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.2.2) - actionpack (>= 4.0) - activesupport (>= 4.0) - sprockets (>= 3.0.0) - thor (1.0.1) - tzinfo (2.0.4) - concurrent-ruby (~> 1.0) - websocket-driver (0.7.3) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.5) - zeitwerk (2.4.2) - -PLATFORMS - ruby - -DEPENDENCIES - activerecord (~> 6.1.0) - activerecord-multi-tenant! - appraisal - pg - pry - pry-byebug - rake - rspec (>= 3.0) - rspec-rails - sidekiq - thor - -BUNDLED WITH - 2.2.22 diff --git a/gemfiles/active_record_7.0.gemfile.lock b/gemfiles/active_record_7.0.gemfile.lock deleted file mode 100644 index 7c89dd24..00000000 --- a/gemfiles/active_record_7.0.gemfile.lock +++ /dev/null @@ -1,191 +0,0 @@ -PATH - remote: .. - specs: - activerecord-multi-tenant (1.1.1) - rails (>= 4.2) - request_store (>= 1.0.5) - -GEM - remote: https://rubygems.org/ - specs: - actioncable (7.0.0) - actionpack (= 7.0.0) - activesupport (= 7.0.0) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - actionmailbox (7.0.0) - actionpack (= 7.0.0) - activejob (= 7.0.0) - activerecord (= 7.0.0) - activestorage (= 7.0.0) - activesupport (= 7.0.0) - mail (>= 2.7.1) - actionmailer (7.0.0) - actionpack (= 7.0.0) - actionview (= 7.0.0) - activejob (= 7.0.0) - activesupport (= 7.0.0) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (7.0.0) - actionview (= 7.0.0) - activesupport (= 7.0.0) - rack (~> 2.0, >= 2.2.0) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.0) - actionpack (= 7.0.0) - activerecord (= 7.0.0) - activestorage (= 7.0.0) - activesupport (= 7.0.0) - globalid (>= 0.6.0) - nokogiri (>= 1.8.5) - actionview (7.0.0) - activesupport (= 7.0.0) - builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (7.0.0) - activesupport (= 7.0.0) - globalid (>= 0.3.6) - activemodel (7.0.0) - activesupport (= 7.0.0) - activerecord (7.0.0) - activemodel (= 7.0.0) - activesupport (= 7.0.0) - activestorage (7.0.0) - actionpack (= 7.0.0) - activejob (= 7.0.0) - activerecord (= 7.0.0) - activesupport (= 7.0.0) - marcel (~> 1.0) - mini_mime (>= 1.1.0) - activesupport (7.0.0) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 1.6, < 2) - minitest (>= 5.1) - tzinfo (~> 2.0) - appraisal (2.4.1) - bundler - rake - thor (>= 0.14.0) - builder (3.2.4) - byebug (11.1.3) - coderay (1.1.3) - concurrent-ruby (1.1.9) - connection_pool (2.2.5) - crass (1.0.6) - diff-lcs (1.4.4) - erubi (1.10.0) - globalid (1.0.0) - activesupport (>= 5.0) - i18n (1.8.11) - concurrent-ruby (~> 1.0) - loofah (2.13.0) - crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) - mini_mime (>= 0.1.1) - marcel (1.0.2) - method_source (1.0.0) - mini_mime (1.1.2) - mini_portile2 (2.8.0) - minitest (5.15.0) - nio4r (2.5.8) - nokogiri (1.12.5) - mini_portile2 (~> 2.8.0) - racc (~> 1.4) - pg (1.2.3) - pry (0.13.1) - coderay (~> 1.1) - method_source (~> 1.0) - pry-byebug (3.9.0) - byebug (~> 11.0) - pry (~> 0.13.0) - racc (1.6.0) - rack (2.2.3) - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (7.0.0) - actioncable (= 7.0.0) - actionmailbox (= 7.0.0) - actionmailer (= 7.0.0) - actionpack (= 7.0.0) - actiontext (= 7.0.0) - actionview (= 7.0.0) - activejob (= 7.0.0) - activemodel (= 7.0.0) - activerecord (= 7.0.0) - activestorage (= 7.0.0) - activesupport (= 7.0.0) - bundler (>= 1.15.0) - railties (= 7.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) - nokogiri (>= 1.6) - rails-html-sanitizer (1.4.2) - loofah (~> 2.3) - railties (7.0.0) - actionpack (= 7.0.0) - activesupport (= 7.0.0) - method_source - rake (>= 12.2) - thor (~> 1.0) - zeitwerk (~> 2.5) - rake (13.0.6) - redis (4.5.1) - request_store (1.5.0) - rack (>= 1.4) - rspec (3.10.0) - rspec-core (~> 3.10.0) - rspec-expectations (~> 3.10.0) - rspec-mocks (~> 3.10.0) - rspec-core (3.10.1) - rspec-support (~> 3.10.0) - rspec-expectations (3.10.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-mocks (3.10.2) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-rails (5.0.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - railties (>= 5.2) - rspec-core (~> 3.10) - rspec-expectations (~> 3.10) - rspec-mocks (~> 3.10) - rspec-support (~> 3.10) - rspec-support (3.10.3) - sidekiq (6.3.1) - connection_pool (>= 2.2.2) - rack (~> 2.0) - redis (>= 4.2.0) - thor (1.1.0) - tzinfo (2.0.4) - concurrent-ruby (~> 1.0) - websocket-driver (0.7.5) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.5) - zeitwerk (2.5.1) - -PLATFORMS - ruby - -DEPENDENCIES - activerecord (~> 7.0.0) - activerecord-multi-tenant! - appraisal - pg - pry - pry-byebug - rake - rspec (>= 3.0) - rspec-rails - sidekiq - thor - -BUNDLED WITH - 2.2.22 diff --git a/gemfiles/rails_5.2.gemfile.lock b/gemfiles/rails_5.2.gemfile.lock deleted file mode 100644 index 066002b1..00000000 --- a/gemfiles/rails_5.2.gemfile.lock +++ /dev/null @@ -1,190 +0,0 @@ -PATH - remote: .. - specs: - activerecord-multi-tenant (1.1.1) - rails (>= 4.2) - request_store (>= 1.0.5) - -GEM - remote: https://rubygems.org/ - specs: - actioncable (5.2.3) - actionpack (= 5.2.3) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - actionmailer (5.2.3) - actionpack (= 5.2.3) - actionview (= 5.2.3) - activejob (= 5.2.3) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (5.2.3) - actionview (= 5.2.3) - activesupport (= 5.2.3) - rack (~> 2.0) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.3) - activesupport (= 5.2.3) - builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (5.2.3) - activesupport (= 5.2.3) - globalid (>= 0.3.6) - activemodel (5.2.3) - activesupport (= 5.2.3) - activerecord (5.2.3) - activemodel (= 5.2.3) - activesupport (= 5.2.3) - arel (>= 9.0) - activestorage (5.2.3) - actionpack (= 5.2.3) - activerecord (= 5.2.3) - marcel (~> 0.3.1) - activesupport (5.2.3) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - appraisal (2.2.0) - bundler - rake - thor (>= 0.14.0) - arel (9.0.0) - builder (3.2.4) - byebug (11.1.3) - coderay (1.1.2) - concurrent-ruby (1.1.6) - connection_pool (2.2.2) - crass (1.0.6) - diff-lcs (1.3) - erubi (1.9.0) - globalid (0.4.2) - activesupport (>= 4.2.0) - i18n (0.9.5) - concurrent-ruby (~> 1.0) - loofah (2.5.0) - crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) - mini_mime (>= 0.1.1) - marcel (0.3.3) - mimemagic (~> 0.3.2) - method_source (1.0.0) - mimemagic (0.3.10) - nokogiri (~> 1) - rake - mini_mime (1.0.2) - mini_portile2 (2.1.0) - minitest (5.14.4) - nio4r (2.3.1) - nokogiri (1.7.2) - mini_portile2 (~> 2.1.0) - pg (1.2.3) - pry (0.13.1) - coderay (~> 1.1) - method_source (~> 1.0) - pry-byebug (3.9.0) - byebug (~> 11.0) - pry (~> 0.13.0) - rack (2.2.2) - rack-protection (2.0.8.1) - rack - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (5.2.3) - actioncable (= 5.2.3) - actionmailer (= 5.2.3) - actionpack (= 5.2.3) - actionview (= 5.2.3) - activejob (= 5.2.3) - activemodel (= 5.2.3) - activerecord (= 5.2.3) - activestorage (= 5.2.3) - activesupport (= 5.2.3) - bundler (>= 1.3.0) - railties (= 5.2.3) - sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) - nokogiri (>= 1.6) - rails-html-sanitizer (1.3.0) - loofah (~> 2.3) - railties (5.2.3) - actionpack (= 5.2.3) - activesupport (= 5.2.3) - method_source - rake (>= 0.8.7) - thor (>= 0.19.0, < 2.0) - rake (12.0.0) - redis (3.3.3) - request_store (1.5.0) - rack (>= 1.4) - rspec (3.9.0) - rspec-core (~> 3.9.0) - rspec-expectations (~> 3.9.0) - rspec-mocks (~> 3.9.0) - rspec-core (3.9.2) - rspec-support (~> 3.9.3) - rspec-expectations (3.9.2) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-mocks (3.9.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-rails (4.0.1) - actionpack (>= 4.2) - activesupport (>= 4.2) - railties (>= 4.2) - rspec-core (~> 3.9) - rspec-expectations (~> 3.9) - rspec-mocks (~> 3.9) - rspec-support (~> 3.9) - rspec-support (3.9.3) - sidekiq (5.0.4) - concurrent-ruby (~> 1.0) - connection_pool (~> 2.2, >= 2.2.0) - rack-protection (>= 1.5.0) - redis (~> 3.3, >= 3.3.3) - sprockets (3.7.2) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.2.1) - actionpack (>= 4.0) - activesupport (>= 4.0) - sprockets (>= 3.0.0) - thor (1.0.1) - thread_safe (0.3.6) - tzinfo (1.2.7) - thread_safe (~> 0.1) - websocket-driver (0.7.2) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.4) - -PLATFORMS - ruby - -DEPENDENCIES - activerecord-multi-tenant! - appraisal - byebug (~> 11.0) - i18n (~> 0.9.5) - nio4r (~> 2.3.1) - nokogiri (~> 1.7.1) - pg - pry - pry-byebug (= 3.9.0) - rails (~> 5.2.0) - rake (= 12.0.0) - redis (= 3.3.3) - rspec (>= 3.0) - rspec-rails - sidekiq - sprockets (~> 3.7.1) - thor - -BUNDLED WITH - 2.2.22 diff --git a/gemfiles/rails_6.0.gemfile.lock b/gemfiles/rails_6.0.gemfile.lock deleted file mode 100644 index 573ce8cc..00000000 --- a/gemfiles/rails_6.0.gemfile.lock +++ /dev/null @@ -1,202 +0,0 @@ -PATH - remote: .. - specs: - activerecord-multi-tenant (1.1.1) - rails (>= 4.2) - request_store (>= 1.0.5) - -GEM - remote: https://rubygems.org/ - specs: - actioncable (6.0.3.1) - actionpack (= 6.0.3.1) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - actionmailbox (6.0.3.1) - actionpack (= 6.0.3.1) - activejob (= 6.0.3.1) - activerecord (= 6.0.3.1) - activestorage (= 6.0.3.1) - activesupport (= 6.0.3.1) - mail (>= 2.7.1) - actionmailer (6.0.3.1) - actionpack (= 6.0.3.1) - actionview (= 6.0.3.1) - activejob (= 6.0.3.1) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (6.0.3.1) - actionview (= 6.0.3.1) - activesupport (= 6.0.3.1) - rack (~> 2.0, >= 2.0.8) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.0.3.1) - actionpack (= 6.0.3.1) - activerecord (= 6.0.3.1) - activestorage (= 6.0.3.1) - activesupport (= 6.0.3.1) - nokogiri (>= 1.8.5) - actionview (6.0.3.1) - activesupport (= 6.0.3.1) - builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.0.3.1) - activesupport (= 6.0.3.1) - globalid (>= 0.3.6) - activemodel (6.0.3.1) - activesupport (= 6.0.3.1) - activerecord (6.0.3.1) - activemodel (= 6.0.3.1) - activesupport (= 6.0.3.1) - activestorage (6.0.3.1) - actionpack (= 6.0.3.1) - activejob (= 6.0.3.1) - activerecord (= 6.0.3.1) - marcel (~> 0.3.1) - activesupport (6.0.3.1) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - zeitwerk (~> 2.2, >= 2.2.2) - appraisal (2.2.0) - bundler - rake - thor (>= 0.14.0) - builder (3.2.4) - byebug (11.1.3) - coderay (1.1.2) - concurrent-ruby (1.1.6) - connection_pool (2.2.2) - crass (1.0.6) - diff-lcs (1.3) - erubi (1.9.0) - globalid (0.4.2) - activesupport (>= 4.2.0) - i18n (1.8.2) - concurrent-ruby (~> 1.0) - loofah (2.5.0) - crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) - mini_mime (>= 0.1.1) - marcel (0.3.3) - mimemagic (~> 0.3.2) - method_source (1.0.0) - mimemagic (0.3.10) - nokogiri (~> 1) - rake - mini_mime (1.0.2) - mini_portile2 (2.6.1) - minitest (5.14.4) - nio4r (2.5.2) - nokogiri (1.12.5) - mini_portile2 (~> 2.6.1) - racc (~> 1.4) - pg (1.2.3) - pry (0.13.1) - coderay (~> 1.1) - method_source (~> 1.0) - pry-byebug (3.9.0) - byebug (~> 11.0) - pry (~> 0.13.0) - racc (1.6.0) - rack (2.2.2) - rack-protection (2.0.8.1) - rack - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (6.0.3.1) - actioncable (= 6.0.3.1) - actionmailbox (= 6.0.3.1) - actionmailer (= 6.0.3.1) - actionpack (= 6.0.3.1) - actiontext (= 6.0.3.1) - actionview (= 6.0.3.1) - activejob (= 6.0.3.1) - activemodel (= 6.0.3.1) - activerecord (= 6.0.3.1) - activestorage (= 6.0.3.1) - activesupport (= 6.0.3.1) - bundler (>= 1.3.0) - railties (= 6.0.3.1) - sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) - nokogiri (>= 1.6) - rails-html-sanitizer (1.3.0) - loofah (~> 2.3) - railties (6.0.3.1) - actionpack (= 6.0.3.1) - activesupport (= 6.0.3.1) - method_source - rake (>= 0.8.7) - thor (>= 0.20.3, < 2.0) - rake (13.0.6) - redis (4.1.4) - request_store (1.5.0) - rack (>= 1.4) - rspec (3.9.0) - rspec-core (~> 3.9.0) - rspec-expectations (~> 3.9.0) - rspec-mocks (~> 3.9.0) - rspec-core (3.9.2) - rspec-support (~> 3.9.3) - rspec-expectations (3.9.2) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-mocks (3.9.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-rails (4.0.1) - actionpack (>= 4.2) - activesupport (>= 4.2) - railties (>= 4.2) - rspec-core (~> 3.9) - rspec-expectations (~> 3.9) - rspec-mocks (~> 3.9) - rspec-support (~> 3.9) - rspec-support (3.9.3) - sidekiq (6.0.7) - connection_pool (>= 2.2.2) - rack (~> 2.0) - rack-protection (>= 2.0.0) - redis (>= 4.1.0) - sprockets (4.0.0) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.2.1) - actionpack (>= 4.0) - activesupport (>= 4.0) - sprockets (>= 3.0.0) - thor (1.0.1) - thread_safe (0.3.6) - tzinfo (1.2.7) - thread_safe (~> 0.1) - websocket-driver (0.7.2) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.4) - zeitwerk (2.3.0) - -PLATFORMS - ruby - -DEPENDENCIES - activerecord-multi-tenant! - appraisal - pg - pry - pry-byebug - rails (~> 6.0.3) - rake - rspec (>= 3.0) - rspec-rails - sidekiq - thor - -BUNDLED WITH - 2.2.22 diff --git a/gemfiles/rails_6.1.gemfile.lock b/gemfiles/rails_6.1.gemfile.lock deleted file mode 100644 index 03a1fe9f..00000000 --- a/gemfiles/rails_6.1.gemfile.lock +++ /dev/null @@ -1,202 +0,0 @@ -PATH - remote: .. - specs: - activerecord-multi-tenant (1.1.1) - rails (>= 4.2) - request_store (>= 1.0.5) - -GEM - remote: https://rubygems.org/ - specs: - actioncable (6.1.0) - actionpack (= 6.1.0) - activesupport (= 6.1.0) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - actionmailbox (6.1.0) - actionpack (= 6.1.0) - activejob (= 6.1.0) - activerecord (= 6.1.0) - activestorage (= 6.1.0) - activesupport (= 6.1.0) - mail (>= 2.7.1) - actionmailer (6.1.0) - actionpack (= 6.1.0) - actionview (= 6.1.0) - activejob (= 6.1.0) - activesupport (= 6.1.0) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (6.1.0) - actionview (= 6.1.0) - activesupport (= 6.1.0) - rack (~> 2.0, >= 2.0.9) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.0) - actionpack (= 6.1.0) - activerecord (= 6.1.0) - activestorage (= 6.1.0) - activesupport (= 6.1.0) - nokogiri (>= 1.8.5) - actionview (6.1.0) - activesupport (= 6.1.0) - builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.1.0) - activesupport (= 6.1.0) - globalid (>= 0.3.6) - activemodel (6.1.0) - activesupport (= 6.1.0) - activerecord (6.1.0) - activemodel (= 6.1.0) - activesupport (= 6.1.0) - activestorage (6.1.0) - actionpack (= 6.1.0) - activejob (= 6.1.0) - activerecord (= 6.1.0) - activesupport (= 6.1.0) - marcel (~> 0.3.1) - mimemagic (~> 0.3.2) - activesupport (6.1.0) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 1.6, < 2) - minitest (>= 5.1) - tzinfo (~> 2.0) - zeitwerk (~> 2.3) - appraisal (2.3.0) - bundler - rake - thor (>= 0.14.0) - builder (3.2.4) - byebug (11.1.3) - coderay (1.1.3) - concurrent-ruby (1.1.7) - connection_pool (2.2.3) - crass (1.0.6) - diff-lcs (1.4.4) - erubi (1.10.0) - globalid (0.4.2) - activesupport (>= 4.2.0) - i18n (1.8.5) - concurrent-ruby (~> 1.0) - loofah (2.8.0) - crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) - mini_mime (>= 0.1.1) - marcel (0.3.3) - mimemagic (~> 0.3.2) - method_source (1.0.0) - mimemagic (0.3.10) - nokogiri (~> 1) - rake - mini_mime (1.0.2) - mini_portile2 (2.6.1) - minitest (5.14.4) - nio4r (2.5.4) - nokogiri (1.12.5) - mini_portile2 (~> 2.6.1) - racc (~> 1.4) - pg (1.2.3) - pry (0.13.1) - coderay (~> 1.1) - method_source (~> 1.0) - pry-byebug (3.9.0) - byebug (~> 11.0) - pry (~> 0.13.0) - racc (1.6.0) - rack (2.2.3) - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (6.1.0) - actioncable (= 6.1.0) - actionmailbox (= 6.1.0) - actionmailer (= 6.1.0) - actionpack (= 6.1.0) - actiontext (= 6.1.0) - actionview (= 6.1.0) - activejob (= 6.1.0) - activemodel (= 6.1.0) - activerecord (= 6.1.0) - activestorage (= 6.1.0) - activesupport (= 6.1.0) - bundler (>= 1.15.0) - railties (= 6.1.0) - sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) - nokogiri (>= 1.6) - rails-html-sanitizer (1.3.0) - loofah (~> 2.3) - railties (6.1.0) - actionpack (= 6.1.0) - activesupport (= 6.1.0) - method_source - rake (>= 0.8.7) - thor (~> 1.0) - rake (13.0.6) - redis (4.2.5) - request_store (1.5.0) - rack (>= 1.4) - rspec (3.10.0) - rspec-core (~> 3.10.0) - rspec-expectations (~> 3.10.0) - rspec-mocks (~> 3.10.0) - rspec-core (3.10.1) - rspec-support (~> 3.10.0) - rspec-expectations (3.10.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-mocks (3.10.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-rails (4.0.2) - actionpack (>= 4.2) - activesupport (>= 4.2) - railties (>= 4.2) - rspec-core (~> 3.10) - rspec-expectations (~> 3.10) - rspec-mocks (~> 3.10) - rspec-support (~> 3.10) - rspec-support (3.10.1) - sidekiq (6.1.2) - connection_pool (>= 2.2.2) - rack (~> 2.0) - redis (>= 4.2.0) - sprockets (4.0.2) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.2.2) - actionpack (>= 4.0) - activesupport (>= 4.0) - sprockets (>= 3.0.0) - thor (1.0.1) - tzinfo (2.0.4) - concurrent-ruby (~> 1.0) - websocket-driver (0.7.3) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.5) - zeitwerk (2.4.2) - -PLATFORMS - ruby - -DEPENDENCIES - activerecord-multi-tenant! - appraisal - pg - pry - pry-byebug - rails (~> 6.1.0) - rake - rspec (>= 3.0) - rspec-rails - sidekiq - thor - -BUNDLED WITH - 2.2.22 diff --git a/gemfiles/rails_7.0.gemfile.lock b/gemfiles/rails_7.0.gemfile.lock deleted file mode 100644 index f939e66e..00000000 --- a/gemfiles/rails_7.0.gemfile.lock +++ /dev/null @@ -1,191 +0,0 @@ -PATH - remote: .. - specs: - activerecord-multi-tenant (1.1.1) - rails (>= 4.2) - request_store (>= 1.0.5) - -GEM - remote: https://rubygems.org/ - specs: - actioncable (7.0.0) - actionpack (= 7.0.0) - activesupport (= 7.0.0) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - actionmailbox (7.0.0) - actionpack (= 7.0.0) - activejob (= 7.0.0) - activerecord (= 7.0.0) - activestorage (= 7.0.0) - activesupport (= 7.0.0) - mail (>= 2.7.1) - actionmailer (7.0.0) - actionpack (= 7.0.0) - actionview (= 7.0.0) - activejob (= 7.0.0) - activesupport (= 7.0.0) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (7.0.0) - actionview (= 7.0.0) - activesupport (= 7.0.0) - rack (~> 2.0, >= 2.2.0) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.0) - actionpack (= 7.0.0) - activerecord (= 7.0.0) - activestorage (= 7.0.0) - activesupport (= 7.0.0) - globalid (>= 0.6.0) - nokogiri (>= 1.8.5) - actionview (7.0.0) - activesupport (= 7.0.0) - builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (7.0.0) - activesupport (= 7.0.0) - globalid (>= 0.3.6) - activemodel (7.0.0) - activesupport (= 7.0.0) - activerecord (7.0.0) - activemodel (= 7.0.0) - activesupport (= 7.0.0) - activestorage (7.0.0) - actionpack (= 7.0.0) - activejob (= 7.0.0) - activerecord (= 7.0.0) - activesupport (= 7.0.0) - marcel (~> 1.0) - mini_mime (>= 1.1.0) - activesupport (7.0.0) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 1.6, < 2) - minitest (>= 5.1) - tzinfo (~> 2.0) - appraisal (2.4.1) - bundler - rake - thor (>= 0.14.0) - builder (3.2.4) - byebug (11.1.3) - coderay (1.1.3) - concurrent-ruby (1.1.9) - connection_pool (2.2.5) - crass (1.0.6) - diff-lcs (1.4.4) - erubi (1.10.0) - globalid (1.0.0) - activesupport (>= 5.0) - i18n (1.8.11) - concurrent-ruby (~> 1.0) - loofah (2.13.0) - crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) - mini_mime (>= 0.1.1) - marcel (1.0.2) - method_source (1.0.0) - mini_mime (1.1.2) - mini_portile2 (2.8.0) - minitest (5.15.0) - nio4r (2.5.8) - nokogiri (1.12.5) - mini_portile2 (~> 2.8.0) - racc (~> 1.4) - pg (1.2.3) - pry (0.13.1) - coderay (~> 1.1) - method_source (~> 1.0) - pry-byebug (3.9.0) - byebug (~> 11.0) - pry (~> 0.13.0) - racc (1.6.0) - rack (2.2.3) - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (7.0.0) - actioncable (= 7.0.0) - actionmailbox (= 7.0.0) - actionmailer (= 7.0.0) - actionpack (= 7.0.0) - actiontext (= 7.0.0) - actionview (= 7.0.0) - activejob (= 7.0.0) - activemodel (= 7.0.0) - activerecord (= 7.0.0) - activestorage (= 7.0.0) - activesupport (= 7.0.0) - bundler (>= 1.15.0) - railties (= 7.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) - nokogiri (>= 1.6) - rails-html-sanitizer (1.4.2) - loofah (~> 2.3) - railties (7.0.0) - actionpack (= 7.0.0) - activesupport (= 7.0.0) - method_source - rake (>= 12.2) - thor (~> 1.0) - zeitwerk (~> 2.5) - rake (13.0.6) - redis (4.5.1) - request_store (1.5.0) - rack (>= 1.4) - rspec (3.10.0) - rspec-core (~> 3.10.0) - rspec-expectations (~> 3.10.0) - rspec-mocks (~> 3.10.0) - rspec-core (3.10.1) - rspec-support (~> 3.10.0) - rspec-expectations (3.10.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-mocks (3.10.2) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-rails (5.0.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - railties (>= 5.2) - rspec-core (~> 3.10) - rspec-expectations (~> 3.10) - rspec-mocks (~> 3.10) - rspec-support (~> 3.10) - rspec-support (3.10.3) - sidekiq (6.3.1) - connection_pool (>= 2.2.2) - rack (~> 2.0) - redis (>= 4.2.0) - thor (1.1.0) - tzinfo (2.0.4) - concurrent-ruby (~> 1.0) - websocket-driver (0.7.5) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.5) - zeitwerk (2.5.1) - -PLATFORMS - ruby - -DEPENDENCIES - activerecord-multi-tenant! - appraisal - pg - pry - pry-byebug - rails (~> 7.0.0) - rake - rspec (>= 3.0) - rspec-rails - sidekiq - thor - -BUNDLED WITH - 2.2.22 From 1dab7d2aa1903cd94effb3e8eba5e5d8ca8321c2 Mon Sep 17 00:00:00 2001 From: Matt Larraz Date: Thu, 24 Mar 2022 12:50:04 -0400 Subject: [PATCH 018/105] Force Rails 5.2 to be < 5.2.4 to unblock the build This needs to be addressed separately --- gemfiles/active_record_5.2.gemfile | 2 +- gemfiles/rails_5.2.gemfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gemfiles/active_record_5.2.gemfile b/gemfiles/active_record_5.2.gemfile index 40886171..59d69767 100644 --- a/gemfiles/active_record_5.2.gemfile +++ b/gemfiles/active_record_5.2.gemfile @@ -3,7 +3,7 @@ source "https://rubygems.org" gem "appraisal" -gem "activerecord", "~> 5.2.0" +gem "activerecord", "~> 5.2.0", "< 5.2.4" # FIXME gem "i18n", "~> 0.9.5" gem "nokogiri", "~> 1.7.1" gem "nio4r", "~> 2.3.1" diff --git a/gemfiles/rails_5.2.gemfile b/gemfiles/rails_5.2.gemfile index 3124d81a..5c24f073 100644 --- a/gemfiles/rails_5.2.gemfile +++ b/gemfiles/rails_5.2.gemfile @@ -3,7 +3,7 @@ source "https://rubygems.org" gem "appraisal" -gem "rails", "~> 5.2.0" +gem "rails", "~> 5.2.0", "< 5.2.4" # FIXME gem "i18n", "~> 0.9.5" gem "nokogiri", "~> 1.7.1" gem "nio4r", "~> 2.3.1" From bd578b6b783b29569fc55bbe98ac266d873ca4f6 Mon Sep 17 00:00:00 2001 From: Matt Larraz Date: Thu, 24 Mar 2022 17:14:19 -0400 Subject: [PATCH 019/105] Replace RequestStore with CurrentAttributes --- activerecord-multi-tenant.gemspec | 3 +-- lib/activerecord-multi-tenant/multi_tenant.rb | 10 +++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/activerecord-multi-tenant.gemspec b/activerecord-multi-tenant.gemspec index 0a0bf9c8..ac46476b 100644 --- a/activerecord-multi-tenant.gemspec +++ b/activerecord-multi-tenant.gemspec @@ -15,8 +15,7 @@ Gem::Specification.new do |s| s.homepage = 'https://github.com/citusdata/activerecord-multi-tenant' s.license = 'MIT' - s.add_runtime_dependency('request_store', '>= 1.0.5') - s.add_dependency('rails','>= 4.2') + s.add_dependency 'rails', '>= 5.2' s.add_development_dependency 'rspec', '>= 3.0' s.add_development_dependency 'rspec-rails' diff --git a/lib/activerecord-multi-tenant/multi_tenant.rb b/lib/activerecord-multi-tenant/multi_tenant.rb index ec643977..5d225cbf 100644 --- a/lib/activerecord-multi-tenant/multi_tenant.rb +++ b/lib/activerecord-multi-tenant/multi_tenant.rb @@ -1,6 +1,10 @@ -require 'request_store' +require 'active_support/current_attributes' module MultiTenant + class Current < ::ActiveSupport::CurrentAttributes + attribute :tenant + end + def self.tenant_klass_defined?(tenant_name) !!tenant_name.to_s.classify.safe_constantize end @@ -43,11 +47,11 @@ def self.multi_tenant_model_for_arel(arel) end def self.current_tenant=(tenant) - RequestStore.store[:current_tenant] = tenant + Current.tenant = tenant end def self.current_tenant - RequestStore.store[:current_tenant] + Current.tenant end def self.current_tenant_id From 4d0d41df14e663998a49c49f6ce1c6110a7d23f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Tue, 29 Mar 2022 19:17:14 +0000 Subject: [PATCH 020/105] 1.2.0 --- CHANGELOG.md | 8 ++++++++ lib/activerecord-multi-tenant/version.rb | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 045d1b6a..2ce73e0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 1.2.0 2022-03-29 + +* Test Rails 7 & Ruby 3 +* Fix regression in 1.1.1 involving deleted tenants [#123](https://github.com/citusdata/activerecord-multi-tenant/pull/123) +* Fix incorrect SQL generated when joining two models and one has a default scope [#132](https://github.com/citusdata/activerecord-multi-tenant/pull/132) +* Update for Rails 5+ removal of type_cast_for_database [#135](https://github.com/citusdata/activerecord-multi-tenant/pull/135) + + ## 1.1.1 2021-01-15 * Add support for Rails 6.1 [#108](https://github.com/citusdata/activerecord-multi-tenant/pull/108) diff --git a/lib/activerecord-multi-tenant/version.rb b/lib/activerecord-multi-tenant/version.rb index eb2deee5..9d8f6e9f 100644 --- a/lib/activerecord-multi-tenant/version.rb +++ b/lib/activerecord-multi-tenant/version.rb @@ -1,3 +1,3 @@ module MultiTenant - VERSION = '1.1.1' + VERSION = '1.2.0' end From 9360c8bd8eaace587028b22582528031d93d7ab1 Mon Sep 17 00:00:00 2001 From: alpaca-tc Date: Mon, 31 Jan 2022 14:53:42 +0900 Subject: [PATCH 021/105] Fixed bug when changing table_name after calling multi_tenant. --- .../model_extensions.rb | 4 +- lib/activerecord-multi-tenant/multi_tenant.rb | 21 +++++++--- .../model_extensions_spec.rb | 41 +++++++++++++++++++ spec/schema.rb | 6 +++ 4 files changed, 65 insertions(+), 7 deletions(-) diff --git a/lib/activerecord-multi-tenant/model_extensions.rb b/lib/activerecord-multi-tenant/model_extensions.rb index 369c1718..8b6037c1 100644 --- a/lib/activerecord-multi-tenant/model_extensions.rb +++ b/lib/activerecord-multi-tenant/model_extensions.rb @@ -38,11 +38,11 @@ def primary_key def inherited(subclass) super - MultiTenant.register_multi_tenant_model(subclass.table_name, subclass) if subclass.table_name + MultiTenant.register_multi_tenant_model(subclass.table_name, subclass) end end - MultiTenant.register_multi_tenant_model(table_name, self) if table_name + MultiTenant.register_multi_tenant_model(table_name, self) @partition_key = options[:partition_key] || MultiTenant.partition_key(tenant_name) partition_key = @partition_key diff --git a/lib/activerecord-multi-tenant/multi_tenant.rb b/lib/activerecord-multi-tenant/multi_tenant.rb index 5d225cbf..be09d04b 100644 --- a/lib/activerecord-multi-tenant/multi_tenant.rb +++ b/lib/activerecord-multi-tenant/multi_tenant.rb @@ -28,13 +28,24 @@ def self.enable_with_lock_workaround; @@enable_with_lock_workaround = true; end def self.with_lock_workaround_enabled?; @@enable_with_lock_workaround; end # Registry that maps table names to models (used by the query rewriter) - def self.register_multi_tenant_model(table_name, model_klass) - @@multi_tenant_models ||= {} - @@multi_tenant_models[table_name.to_s] = model_klass + # @deprecated _table_name is no longer used + def self.register_multi_tenant_model(_table_name, model_klass) + @@multi_tenant_models ||= [] + @@multi_tenant_models.push(model_klass) + + remove_class_variable(:@@multi_tenant_model_table_names) if defined?(@@multi_tenant_model_table_names) end + def self.multi_tenant_model_for_table(table_name) - @@multi_tenant_models ||= {} - @@multi_tenant_models[table_name.to_s] + @@multi_tenant_models ||= [] + + if !defined?(@@multi_tenant_model_table_names) + @@multi_tenant_model_table_names = @@multi_tenant_models.map { |model| + [model.table_name, model] if model.table_name + }.compact.to_h + end + + @@multi_tenant_model_table_names[table_name.to_s] end def self.multi_tenant_model_for_arel(arel) diff --git a/spec/activerecord-multi-tenant/model_extensions_spec.rb b/spec/activerecord-multi-tenant/model_extensions_spec.rb index 65f2e208..a79198af 100644 --- a/spec/activerecord-multi-tenant/model_extensions_spec.rb +++ b/spec/activerecord-multi-tenant/model_extensions_spec.rb @@ -70,6 +70,47 @@ it { expect(@partition_key_not_model_task.non_model_id).to be 77 } end + describe 'Changes table_name after multi_tenant called' do + before do + account_klass.has_many(:posts, anonymous_class: post_klass) + post_klass.belongs_to(:account, anonymous_class: account_klass) + + @account1 = account_klass.create! name: 'foo' + @account2 = account_klass.create! name: 'bar' + + @post1 = @account1.posts.create! name: 'foobar' + @post2 = @account2.posts.create! name: 'baz' + + MultiTenant.current_tenant = @account1 + @posts = post_klass.all + end + + let(:account_klass) do + Class.new(Account) do + def self.name + 'Account' + end + end + end + + let(:post_klass) do + Class.new(ActiveRecord::Base) do + self.table_name = 'unknown' + + multi_tenant(:account) + + self.table_name = 'posts' + + def self.name + 'Post' + end + end + end + + it { expect(@posts.length).to eq(1) } + it { expect(@posts).to eq([@post1]) } + end + # Scoping models describe 'Project.all should be scoped to the current tenant if set' do before do diff --git a/spec/schema.rb b/spec/schema.rb index 88d80d80..92bfdcbc 100644 --- a/spec/schema.rb +++ b/spec/schema.rb @@ -106,6 +106,11 @@ t.column :domain_id, :integer end + create_table :posts, force: true, partition_key: :account_id do |t| + t.column :account_id, :integer + t.column :name, :string + end + create_distributed_table :accounts, :id create_distributed_table :projects, :account_id create_distributed_table :managers, :account_id @@ -121,6 +126,7 @@ create_distributed_table :allowed_places, :account_id create_distributed_table :domains, :account_id create_distributed_table :pages, :account_id + create_distributed_table :posts, :account_id create_reference_table :categories end From 2b082ffe8a1d4251fca8d08b7caa58ad4b674b80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Mon, 11 Apr 2022 12:40:52 +0000 Subject: [PATCH 022/105] Remove _table_name parameter --- lib/activerecord-multi-tenant/model_extensions.rb | 4 ++-- lib/activerecord-multi-tenant/multi_tenant.rb | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/activerecord-multi-tenant/model_extensions.rb b/lib/activerecord-multi-tenant/model_extensions.rb index 8b6037c1..115280cc 100644 --- a/lib/activerecord-multi-tenant/model_extensions.rb +++ b/lib/activerecord-multi-tenant/model_extensions.rb @@ -38,11 +38,11 @@ def primary_key def inherited(subclass) super - MultiTenant.register_multi_tenant_model(subclass.table_name, subclass) + MultiTenant.register_multi_tenant_model(subclass) end end - MultiTenant.register_multi_tenant_model(table_name, self) + MultiTenant.register_multi_tenant_model(self) @partition_key = options[:partition_key] || MultiTenant.partition_key(tenant_name) partition_key = @partition_key diff --git a/lib/activerecord-multi-tenant/multi_tenant.rb b/lib/activerecord-multi-tenant/multi_tenant.rb index be09d04b..d7412325 100644 --- a/lib/activerecord-multi-tenant/multi_tenant.rb +++ b/lib/activerecord-multi-tenant/multi_tenant.rb @@ -28,8 +28,7 @@ def self.enable_with_lock_workaround; @@enable_with_lock_workaround = true; end def self.with_lock_workaround_enabled?; @@enable_with_lock_workaround; end # Registry that maps table names to models (used by the query rewriter) - # @deprecated _table_name is no longer used - def self.register_multi_tenant_model(_table_name, model_klass) + def self.register_multi_tenant_model(model_klass) @@multi_tenant_models ||= [] @@multi_tenant_models.push(model_klass) From 3d3d64954c1425ff3e9c151d95ac38f659853e31 Mon Sep 17 00:00:00 2001 From: Edson Lima Date: Mon, 18 Jan 2021 00:09:02 -0300 Subject: [PATCH 023/105] Allow to use uuid as primary key on partition table --- lib/activerecord-multi-tenant/model_extensions.rb | 10 +++++++++- spec/schema.rb | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/activerecord-multi-tenant/model_extensions.rb b/lib/activerecord-multi-tenant/model_extensions.rb index 115280cc..e4ebd20c 100644 --- a/lib/activerecord-multi-tenant/model_extensions.rb +++ b/lib/activerecord-multi-tenant/model_extensions.rb @@ -6,7 +6,15 @@ def multi_tenant(tenant_name, options = {}) if to_s.underscore.to_sym == tenant_name unless MultiTenant.with_write_only_mode_enabled? # This is the tenant model itself. Workaround for https://github.com/citusdata/citus/issues/687 - before_create -> { self.id ||= self.class.connection.select_value("SELECT nextval('" + [self.class.table_name, self.class.primary_key, 'seq'].join('_') + "'::regclass)") } + before_create lambda { + self.id ||= if self.class.columns_hash['id'].type == :uuid + SecureRandom.uuid + else + self.class.connection.select_value( + "SELECT nextval('#{[self.class.table_name, self.class.primary_key, 'seq'].join('_')}'::regclass)" + ) + end + } end else class << self diff --git a/spec/schema.rb b/spec/schema.rb index 92bfdcbc..5e7b25be 100644 --- a/spec/schema.rb +++ b/spec/schema.rb @@ -204,6 +204,7 @@ class Comment < ActiveRecord::Base end class Organization < ActiveRecord::Base + multi_tenant :organization has_many :uuid_records end From e053c87a83b34bd550bd8e30e6442dfe55826a2e Mon Sep 17 00:00:00 2001 From: Edson Lima Date: Wed, 16 Mar 2022 17:28:34 -0300 Subject: [PATCH 024/105] Uses self.class.primary_key to get primary key name instead of 'id' --- .../model_extensions.rb | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/activerecord-multi-tenant/model_extensions.rb b/lib/activerecord-multi-tenant/model_extensions.rb index e4ebd20c..e97c47d7 100644 --- a/lib/activerecord-multi-tenant/model_extensions.rb +++ b/lib/activerecord-multi-tenant/model_extensions.rb @@ -6,15 +6,13 @@ def multi_tenant(tenant_name, options = {}) if to_s.underscore.to_sym == tenant_name unless MultiTenant.with_write_only_mode_enabled? # This is the tenant model itself. Workaround for https://github.com/citusdata/citus/issues/687 - before_create lambda { - self.id ||= if self.class.columns_hash['id'].type == :uuid - SecureRandom.uuid - else - self.class.connection.select_value( - "SELECT nextval('#{[self.class.table_name, self.class.primary_key, 'seq'].join('_')}'::regclass)" - ) - end - } + before_create -> do + if self.class.columns_hash[self.class.primary_key].type == :uuid + self.id ||= SecureRandom.uuid + else + self.id ||= self.class.connection.select_value("SELECT nextval('#{self.class.table_name}_#{self.class.primary_key}_seq'::regclass)") + end + end end else class << self From d6af5d7da459715b910db1da3faa30f49c7c1a92 Mon Sep 17 00:00:00 2001 From: Hasan Kumar Date: Thu, 12 May 2022 05:15:00 +0530 Subject: [PATCH 025/105] Fix for latest rails 5.2 See https://github.com/citusdata/activerecord-multi-tenant/pull/116\#issuecomment-1059230711 --- .github/workflows/CI.yml | 8 ++++++++ gemfiles/active_record_5.2.3.gemfile | 16 ++++++++++++++++ gemfiles/active_record_5.2.gemfile | 2 +- gemfiles/rails_5.2.3.gemfile | 16 ++++++++++++++++ gemfiles/rails_5.2.gemfile | 2 +- lib/activerecord-multi-tenant/query_rewriter.rb | 11 ++++------- .../model_extensions_spec.rb | 10 +++++----- 7 files changed, 51 insertions(+), 14 deletions(-) create mode 100644 gemfiles/active_record_5.2.3.gemfile create mode 100644 gemfiles/rails_5.2.3.gemfile diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index d22b6b67..ee3db6a2 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -47,6 +47,14 @@ jobs: gemfile: 'rails_5.2' - ruby: '3.1' gemfile: 'active_record_5.2' + - ruby: '3.0' + gemfile: 'rails_5.2.3' + - ruby: '3.0' + gemfile: 'active_record_5.2.3' + - ruby: '3.1' + gemfile: 'rails_5.2.3' + - ruby: '3.1' + gemfile: 'active_record_5.2.3' name: Ruby ${{ matrix.ruby }} / ${{ matrix.gemfile }} ${{ (matrix.prepared_statements && 'w/ prepared statements') || '' }} env: BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile diff --git a/gemfiles/active_record_5.2.3.gemfile b/gemfiles/active_record_5.2.3.gemfile new file mode 100644 index 00000000..59d69767 --- /dev/null +++ b/gemfiles/active_record_5.2.3.gemfile @@ -0,0 +1,16 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "appraisal" +gem "activerecord", "~> 5.2.0", "< 5.2.4" # FIXME +gem "i18n", "~> 0.9.5" +gem "nokogiri", "~> 1.7.1" +gem "nio4r", "~> 2.3.1" +gem "sprockets", "~> 3.7.1" +gem "byebug", "~> 11.0" +gem "rake", "12.0.0" +gem "redis", "3.3.3" +gem "pry-byebug", "3.9.0" + +gemspec path: "../" diff --git a/gemfiles/active_record_5.2.gemfile b/gemfiles/active_record_5.2.gemfile index 59d69767..40886171 100644 --- a/gemfiles/active_record_5.2.gemfile +++ b/gemfiles/active_record_5.2.gemfile @@ -3,7 +3,7 @@ source "https://rubygems.org" gem "appraisal" -gem "activerecord", "~> 5.2.0", "< 5.2.4" # FIXME +gem "activerecord", "~> 5.2.0" gem "i18n", "~> 0.9.5" gem "nokogiri", "~> 1.7.1" gem "nio4r", "~> 2.3.1" diff --git a/gemfiles/rails_5.2.3.gemfile b/gemfiles/rails_5.2.3.gemfile new file mode 100644 index 00000000..5c24f073 --- /dev/null +++ b/gemfiles/rails_5.2.3.gemfile @@ -0,0 +1,16 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "appraisal" +gem "rails", "~> 5.2.0", "< 5.2.4" # FIXME +gem "i18n", "~> 0.9.5" +gem "nokogiri", "~> 1.7.1" +gem "nio4r", "~> 2.3.1" +gem "sprockets", "~> 3.7.1" +gem "byebug", "~> 11.0" +gem "rake", "12.0.0" +gem "redis", "3.3.3" +gem "pry-byebug", "3.9.0" + +gemspec path: "../" diff --git a/gemfiles/rails_5.2.gemfile b/gemfiles/rails_5.2.gemfile index 5c24f073..3124d81a 100644 --- a/gemfiles/rails_5.2.gemfile +++ b/gemfiles/rails_5.2.gemfile @@ -3,7 +3,7 @@ source "https://rubygems.org" gem "appraisal" -gem "rails", "~> 5.2.0", "< 5.2.4" # FIXME +gem "rails", "~> 5.2.0" gem "i18n", "~> 0.9.5" gem "nokogiri", "~> 1.7.1" gem "nio4r", "~> 2.3.1" diff --git a/lib/activerecord-multi-tenant/query_rewriter.rb b/lib/activerecord-multi-tenant/query_rewriter.rb index 707f8603..efd1d4f9 100644 --- a/lib/activerecord-multi-tenant/query_rewriter.rb +++ b/lib/activerecord-multi-tenant/query_rewriter.rb @@ -295,12 +295,9 @@ def build_arel(*args) end node_list.select{ |n| n.is_a? Arel::Nodes::Join }.each do |node_join| - if (!node_join.right || - (ActiveRecord::VERSION::MAJOR == 5 && - !node_join.right.expr.right.is_a?(Arel::Attributes::Attribute))) + if (!node_join.right) next end - relation_right, relation_left = relations_from_node_join(node_join) next unless relation_right && relation_left @@ -322,13 +319,13 @@ def build_arel(*args) private def relations_from_node_join(node_join) - if ActiveRecord::VERSION::MAJOR == 5 || node_join.right.expr.is_a?(Arel::Nodes::Equality) + if node_join.right.expr.is_a?(Arel::Nodes::Equality) return node_join.right.expr.right.relation, node_join.right.expr.left.relation end - children = node_join.right.expr.children + children = [node_join.right.expr.children].flatten - tenant_applied = children.any?(MultiTenant::TenantEnforcementClause) || children.any?(MultiTenant::TenantJoinEnforcementClause) + tenant_applied = children.any?{|c| c.any?(MultiTenant::TenantEnforcementClause) || c.any?(MultiTenant::TenantJoinEnforcementClause)} if tenant_applied || children.empty? return nil, nil end diff --git a/spec/activerecord-multi-tenant/model_extensions_spec.rb b/spec/activerecord-multi-tenant/model_extensions_spec.rb index a79198af..9315daf2 100644 --- a/spec/activerecord-multi-tenant/model_extensions_spec.rb +++ b/spec/activerecord-multi-tenant/model_extensions_spec.rb @@ -528,13 +528,13 @@ def self.name MultiTenant.with(account) do option1 = <<-sql.strip - SELECT "projects".* FROM "projects" WHERE "projects"."account_id" = #{account.id} AND "projects"."id" = $1 LIMIT $2 + SELECT "projects".* FROM "projects" WHERE "projects"."account_id" = #{account.id} AND "projects"."id" = #{project.id} LIMIT 1 sql option2 = <<-sql.strip - SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 AND "projects"."account_id" = #{account.id} LIMIT $2 + SELECT "projects".* FROM "projects" WHERE "projects"."id" = #{project.id} AND "projects"."account_id" = #{account.id} LIMIT 1 sql option3 = <<-sql.strip - SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 AND "projects"."account_id" = #{account.id} LIMIT $2 + SELECT "projects".* FROM "projects" WHERE "projects"."id" = #{project.id} AND "projects"."account_id" = #{account.id} LIMIT 1 sql # Couldn't make the following line pass for some reason, so came up with an uglier alternative @@ -548,10 +548,10 @@ def self.name MultiTenant.without do option1 = <<-sql.strip - SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2 + SELECT "projects".* FROM "projects" WHERE "projects"."id" = #{project2.id} LIMIT 1 sql option2 = <<-sql.strip - SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2 + SELECT "projects".* FROM "projects" WHERE "projects"."id" = #{project2.id} LIMIT 1 sql # Couldn't make the following line pass for some reason, so came up with an uglier alternative From 17eebde18a90e90e39adbc9811c6e4923e9a707d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Fri, 13 May 2022 17:38:45 +0000 Subject: [PATCH 026/105] Update lib/activerecord-multi-tenant/query_rewriter.rb --- lib/activerecord-multi-tenant/query_rewriter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/activerecord-multi-tenant/query_rewriter.rb b/lib/activerecord-multi-tenant/query_rewriter.rb index efd1d4f9..72b2b1a4 100644 --- a/lib/activerecord-multi-tenant/query_rewriter.rb +++ b/lib/activerecord-multi-tenant/query_rewriter.rb @@ -295,7 +295,7 @@ def build_arel(*args) end node_list.select{ |n| n.is_a? Arel::Nodes::Join }.each do |node_join| - if (!node_join.right) + if !node_join.right next end relation_right, relation_left = relations_from_node_join(node_join) From db5018a49fa45d341cc1c93183d202c0808d01ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Fri, 13 May 2022 17:47:07 +0000 Subject: [PATCH 027/105] Revert "Update lib/activerecord-multi-tenant/query_rewriter.rb" This reverts commit 17eebde18a90e90e39adbc9811c6e4923e9a707d. --- lib/activerecord-multi-tenant/query_rewriter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/activerecord-multi-tenant/query_rewriter.rb b/lib/activerecord-multi-tenant/query_rewriter.rb index 72b2b1a4..efd1d4f9 100644 --- a/lib/activerecord-multi-tenant/query_rewriter.rb +++ b/lib/activerecord-multi-tenant/query_rewriter.rb @@ -295,7 +295,7 @@ def build_arel(*args) end node_list.select{ |n| n.is_a? Arel::Nodes::Join }.each do |node_join| - if !node_join.right + if (!node_join.right) next end relation_right, relation_left = relations_from_node_join(node_join) From b7bb41b162056195fbf6cb4d41e618bc87d5ac0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Fri, 13 May 2022 17:47:07 +0000 Subject: [PATCH 028/105] Revert "Fix for latest rails 5.2 See https://github.com/citusdata/activerecord-multi-tenant/pull/116\#issuecomment-1059230711" This reverts commit d6af5d7da459715b910db1da3faa30f49c7c1a92. --- .github/workflows/CI.yml | 8 -------- gemfiles/active_record_5.2.3.gemfile | 16 ---------------- gemfiles/active_record_5.2.gemfile | 2 +- gemfiles/rails_5.2.3.gemfile | 16 ---------------- gemfiles/rails_5.2.gemfile | 2 +- lib/activerecord-multi-tenant/query_rewriter.rb | 11 +++++++---- .../model_extensions_spec.rb | 10 +++++----- 7 files changed, 14 insertions(+), 51 deletions(-) delete mode 100644 gemfiles/active_record_5.2.3.gemfile delete mode 100644 gemfiles/rails_5.2.3.gemfile diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index ee3db6a2..d22b6b67 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -47,14 +47,6 @@ jobs: gemfile: 'rails_5.2' - ruby: '3.1' gemfile: 'active_record_5.2' - - ruby: '3.0' - gemfile: 'rails_5.2.3' - - ruby: '3.0' - gemfile: 'active_record_5.2.3' - - ruby: '3.1' - gemfile: 'rails_5.2.3' - - ruby: '3.1' - gemfile: 'active_record_5.2.3' name: Ruby ${{ matrix.ruby }} / ${{ matrix.gemfile }} ${{ (matrix.prepared_statements && 'w/ prepared statements') || '' }} env: BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile diff --git a/gemfiles/active_record_5.2.3.gemfile b/gemfiles/active_record_5.2.3.gemfile deleted file mode 100644 index 59d69767..00000000 --- a/gemfiles/active_record_5.2.3.gemfile +++ /dev/null @@ -1,16 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "appraisal" -gem "activerecord", "~> 5.2.0", "< 5.2.4" # FIXME -gem "i18n", "~> 0.9.5" -gem "nokogiri", "~> 1.7.1" -gem "nio4r", "~> 2.3.1" -gem "sprockets", "~> 3.7.1" -gem "byebug", "~> 11.0" -gem "rake", "12.0.0" -gem "redis", "3.3.3" -gem "pry-byebug", "3.9.0" - -gemspec path: "../" diff --git a/gemfiles/active_record_5.2.gemfile b/gemfiles/active_record_5.2.gemfile index 40886171..59d69767 100644 --- a/gemfiles/active_record_5.2.gemfile +++ b/gemfiles/active_record_5.2.gemfile @@ -3,7 +3,7 @@ source "https://rubygems.org" gem "appraisal" -gem "activerecord", "~> 5.2.0" +gem "activerecord", "~> 5.2.0", "< 5.2.4" # FIXME gem "i18n", "~> 0.9.5" gem "nokogiri", "~> 1.7.1" gem "nio4r", "~> 2.3.1" diff --git a/gemfiles/rails_5.2.3.gemfile b/gemfiles/rails_5.2.3.gemfile deleted file mode 100644 index 5c24f073..00000000 --- a/gemfiles/rails_5.2.3.gemfile +++ /dev/null @@ -1,16 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "appraisal" -gem "rails", "~> 5.2.0", "< 5.2.4" # FIXME -gem "i18n", "~> 0.9.5" -gem "nokogiri", "~> 1.7.1" -gem "nio4r", "~> 2.3.1" -gem "sprockets", "~> 3.7.1" -gem "byebug", "~> 11.0" -gem "rake", "12.0.0" -gem "redis", "3.3.3" -gem "pry-byebug", "3.9.0" - -gemspec path: "../" diff --git a/gemfiles/rails_5.2.gemfile b/gemfiles/rails_5.2.gemfile index 3124d81a..5c24f073 100644 --- a/gemfiles/rails_5.2.gemfile +++ b/gemfiles/rails_5.2.gemfile @@ -3,7 +3,7 @@ source "https://rubygems.org" gem "appraisal" -gem "rails", "~> 5.2.0" +gem "rails", "~> 5.2.0", "< 5.2.4" # FIXME gem "i18n", "~> 0.9.5" gem "nokogiri", "~> 1.7.1" gem "nio4r", "~> 2.3.1" diff --git a/lib/activerecord-multi-tenant/query_rewriter.rb b/lib/activerecord-multi-tenant/query_rewriter.rb index efd1d4f9..707f8603 100644 --- a/lib/activerecord-multi-tenant/query_rewriter.rb +++ b/lib/activerecord-multi-tenant/query_rewriter.rb @@ -295,9 +295,12 @@ def build_arel(*args) end node_list.select{ |n| n.is_a? Arel::Nodes::Join }.each do |node_join| - if (!node_join.right) + if (!node_join.right || + (ActiveRecord::VERSION::MAJOR == 5 && + !node_join.right.expr.right.is_a?(Arel::Attributes::Attribute))) next end + relation_right, relation_left = relations_from_node_join(node_join) next unless relation_right && relation_left @@ -319,13 +322,13 @@ def build_arel(*args) private def relations_from_node_join(node_join) - if node_join.right.expr.is_a?(Arel::Nodes::Equality) + if ActiveRecord::VERSION::MAJOR == 5 || node_join.right.expr.is_a?(Arel::Nodes::Equality) return node_join.right.expr.right.relation, node_join.right.expr.left.relation end - children = [node_join.right.expr.children].flatten + children = node_join.right.expr.children - tenant_applied = children.any?{|c| c.any?(MultiTenant::TenantEnforcementClause) || c.any?(MultiTenant::TenantJoinEnforcementClause)} + tenant_applied = children.any?(MultiTenant::TenantEnforcementClause) || children.any?(MultiTenant::TenantJoinEnforcementClause) if tenant_applied || children.empty? return nil, nil end diff --git a/spec/activerecord-multi-tenant/model_extensions_spec.rb b/spec/activerecord-multi-tenant/model_extensions_spec.rb index 9315daf2..a79198af 100644 --- a/spec/activerecord-multi-tenant/model_extensions_spec.rb +++ b/spec/activerecord-multi-tenant/model_extensions_spec.rb @@ -528,13 +528,13 @@ def self.name MultiTenant.with(account) do option1 = <<-sql.strip - SELECT "projects".* FROM "projects" WHERE "projects"."account_id" = #{account.id} AND "projects"."id" = #{project.id} LIMIT 1 + SELECT "projects".* FROM "projects" WHERE "projects"."account_id" = #{account.id} AND "projects"."id" = $1 LIMIT $2 sql option2 = <<-sql.strip - SELECT "projects".* FROM "projects" WHERE "projects"."id" = #{project.id} AND "projects"."account_id" = #{account.id} LIMIT 1 + SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 AND "projects"."account_id" = #{account.id} LIMIT $2 sql option3 = <<-sql.strip - SELECT "projects".* FROM "projects" WHERE "projects"."id" = #{project.id} AND "projects"."account_id" = #{account.id} LIMIT 1 + SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 AND "projects"."account_id" = #{account.id} LIMIT $2 sql # Couldn't make the following line pass for some reason, so came up with an uglier alternative @@ -548,10 +548,10 @@ def self.name MultiTenant.without do option1 = <<-sql.strip - SELECT "projects".* FROM "projects" WHERE "projects"."id" = #{project2.id} LIMIT 1 + SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2 sql option2 = <<-sql.strip - SELECT "projects".* FROM "projects" WHERE "projects"."id" = #{project2.id} LIMIT 1 + SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2 sql # Couldn't make the following line pass for some reason, so came up with an uglier alternative From 8e2d5be9f9957b9b050c3892387a7f559cec7471 Mon Sep 17 00:00:00 2001 From: Hasan Kumar Date: Thu, 12 May 2022 05:15:00 +0530 Subject: [PATCH 029/105] Fix for latest rails 5.2 See https://github.com/citusdata/activerecord-multi-tenant/pull/116\#issuecomment-1059230711 --- .github/workflows/CI.yml | 10 ++++++++++ gemfiles/active_record_5.2.3.gemfile | 16 ++++++++++++++++ gemfiles/active_record_5.2.gemfile | 2 +- gemfiles/rails_5.2.3.gemfile | 16 ++++++++++++++++ gemfiles/rails_5.2.gemfile | 2 +- lib/activerecord-multi-tenant/query_rewriter.rb | 11 ++++------- 6 files changed, 48 insertions(+), 9 deletions(-) create mode 100644 gemfiles/active_record_5.2.3.gemfile create mode 100644 gemfiles/rails_5.2.3.gemfile diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index d22b6b67..dc729358 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -19,10 +19,12 @@ jobs: - '3.0' - '3.1' gemfile: + - rails_5.2.3 - rails_5.2 - rails_6.0 - rails_6.1 - rails_7.0 + - active_record_5.2.3 - active_record_5.2 - active_record_6.0 - active_record_6.1 @@ -47,6 +49,14 @@ jobs: gemfile: 'rails_5.2' - ruby: '3.1' gemfile: 'active_record_5.2' + - ruby: '3.0' + gemfile: 'rails_5.2.3' + - ruby: '3.0' + gemfile: 'active_record_5.2.3' + - ruby: '3.1' + gemfile: 'rails_5.2.3' + - ruby: '3.1' + gemfile: 'active_record_5.2.3' name: Ruby ${{ matrix.ruby }} / ${{ matrix.gemfile }} ${{ (matrix.prepared_statements && 'w/ prepared statements') || '' }} env: BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile diff --git a/gemfiles/active_record_5.2.3.gemfile b/gemfiles/active_record_5.2.3.gemfile new file mode 100644 index 00000000..59d69767 --- /dev/null +++ b/gemfiles/active_record_5.2.3.gemfile @@ -0,0 +1,16 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "appraisal" +gem "activerecord", "~> 5.2.0", "< 5.2.4" # FIXME +gem "i18n", "~> 0.9.5" +gem "nokogiri", "~> 1.7.1" +gem "nio4r", "~> 2.3.1" +gem "sprockets", "~> 3.7.1" +gem "byebug", "~> 11.0" +gem "rake", "12.0.0" +gem "redis", "3.3.3" +gem "pry-byebug", "3.9.0" + +gemspec path: "../" diff --git a/gemfiles/active_record_5.2.gemfile b/gemfiles/active_record_5.2.gemfile index 59d69767..40886171 100644 --- a/gemfiles/active_record_5.2.gemfile +++ b/gemfiles/active_record_5.2.gemfile @@ -3,7 +3,7 @@ source "https://rubygems.org" gem "appraisal" -gem "activerecord", "~> 5.2.0", "< 5.2.4" # FIXME +gem "activerecord", "~> 5.2.0" gem "i18n", "~> 0.9.5" gem "nokogiri", "~> 1.7.1" gem "nio4r", "~> 2.3.1" diff --git a/gemfiles/rails_5.2.3.gemfile b/gemfiles/rails_5.2.3.gemfile new file mode 100644 index 00000000..5c24f073 --- /dev/null +++ b/gemfiles/rails_5.2.3.gemfile @@ -0,0 +1,16 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "appraisal" +gem "rails", "~> 5.2.0", "< 5.2.4" # FIXME +gem "i18n", "~> 0.9.5" +gem "nokogiri", "~> 1.7.1" +gem "nio4r", "~> 2.3.1" +gem "sprockets", "~> 3.7.1" +gem "byebug", "~> 11.0" +gem "rake", "12.0.0" +gem "redis", "3.3.3" +gem "pry-byebug", "3.9.0" + +gemspec path: "../" diff --git a/gemfiles/rails_5.2.gemfile b/gemfiles/rails_5.2.gemfile index 5c24f073..3124d81a 100644 --- a/gemfiles/rails_5.2.gemfile +++ b/gemfiles/rails_5.2.gemfile @@ -3,7 +3,7 @@ source "https://rubygems.org" gem "appraisal" -gem "rails", "~> 5.2.0", "< 5.2.4" # FIXME +gem "rails", "~> 5.2.0" gem "i18n", "~> 0.9.5" gem "nokogiri", "~> 1.7.1" gem "nio4r", "~> 2.3.1" diff --git a/lib/activerecord-multi-tenant/query_rewriter.rb b/lib/activerecord-multi-tenant/query_rewriter.rb index 707f8603..38f63e13 100644 --- a/lib/activerecord-multi-tenant/query_rewriter.rb +++ b/lib/activerecord-multi-tenant/query_rewriter.rb @@ -295,12 +295,9 @@ def build_arel(*args) end node_list.select{ |n| n.is_a? Arel::Nodes::Join }.each do |node_join| - if (!node_join.right || - (ActiveRecord::VERSION::MAJOR == 5 && - !node_join.right.expr.right.is_a?(Arel::Attributes::Attribute))) + if !node_join.right next end - relation_right, relation_left = relations_from_node_join(node_join) next unless relation_right && relation_left @@ -322,13 +319,13 @@ def build_arel(*args) private def relations_from_node_join(node_join) - if ActiveRecord::VERSION::MAJOR == 5 || node_join.right.expr.is_a?(Arel::Nodes::Equality) + if node_join.right.expr.is_a?(Arel::Nodes::Equality) return node_join.right.expr.right.relation, node_join.right.expr.left.relation end - children = node_join.right.expr.children + children = [node_join.right.expr.children].flatten - tenant_applied = children.any?(MultiTenant::TenantEnforcementClause) || children.any?(MultiTenant::TenantJoinEnforcementClause) + tenant_applied = children.any?{|c| c.is_a?(MultiTenant::TenantEnforcementClause) || c.is_a?(MultiTenant::TenantJoinEnforcementClause)} if tenant_applied || children.empty? return nil, nil end From bcf93ec98a3908e3bfb822a22948089736d3bc23 Mon Sep 17 00:00:00 2001 From: Shayon Mukherjee Date: Wed, 18 May 2022 12:20:35 -0400 Subject: [PATCH 030/105] Rails 5+: Support optional: true for belongs_to Rails5+ onwards belongs_to are required. Setting an optional: true, make record persistence optional and a validation error is not raised. This also makes it easy to rollout the gem on existing models. This change now supports optional: true when overriding the belongs_to association. Since the specs don't quite behave like a traditional rails app, and there are multiple CI matrices, only turning on belongs_to_required_by_default for the respective test, instead of turning it for all tests at the ActiveRecordd::Base layer. Doing so, also makes a few other specs fail (unintentional, since they are testing for something else. For instance, this: https://github.com/shayonj/activerecord-multi-tenant/blob/b7bb41b162056195fbf6cb4d41e618bc87d5ac0f/spec/activerecord-multi-tenant/model_extensions_spec.rb\#L586-L588 would not be true, because the child association is required with belongs_to_required_by_default being true. --- .../model_extensions.rb | 2 +- .../model_extensions_spec.rb | 10 ++++++++++ spec/schema.rb | 17 +++++++++++++++++ spec/spec_helper.rb | 7 +++++++ 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/lib/activerecord-multi-tenant/model_extensions.rb b/lib/activerecord-multi-tenant/model_extensions.rb index e97c47d7..76ed0387 100644 --- a/lib/activerecord-multi-tenant/model_extensions.rb +++ b/lib/activerecord-multi-tenant/model_extensions.rb @@ -55,7 +55,7 @@ def inherited(subclass) # Create an implicit belongs_to association only if tenant class exists if MultiTenant.tenant_klass_defined?(tenant_name) - belongs_to tenant_name, **options.slice(:class_name, :inverse_of).merge(foreign_key: options[:partition_key]) + belongs_to tenant_name, **options.slice(:class_name, :inverse_of, :optional).merge(foreign_key: options[:partition_key]) end # New instances should have the tenant set diff --git a/spec/activerecord-multi-tenant/model_extensions_spec.rb b/spec/activerecord-multi-tenant/model_extensions_spec.rb index a79198af..409e30d9 100644 --- a/spec/activerecord-multi-tenant/model_extensions_spec.rb +++ b/spec/activerecord-multi-tenant/model_extensions_spec.rb @@ -185,6 +185,16 @@ def self.name end end + it 'handles belongs_to with optional: true' do + MultiTenant.with(account) do + sub_task + end + + record = sub_task.optional_sub_tasks.create! + expect(record.reload.sub_task).to eq(sub_task) + expect(record.account_id).to eq(nil) + end + it 'handles has_many through' do MultiTenant.with(account) do expect(project.sub_tasks).to eq [sub_task] diff --git a/spec/schema.rb b/spec/schema.rb index 5e7b25be..dbb515f3 100644 --- a/spec/schema.rb +++ b/spec/schema.rb @@ -34,6 +34,13 @@ t.column :type, :string end + create_table :optional_sub_tasks, force: true do |t| + t.references :account, :integer + t.column :sub_task_id, :integer + t.column :name, :string + t.column :type, :string + end + create_table :countries, force: true do |t| t.column :name, :string end @@ -134,6 +141,7 @@ class Account < ActiveRecord::Base multi_tenant :account has_many :projects has_one :manager, inverse_of: :account + has_many :optional_sub_tasks end class Project < ActiveRecord::Base @@ -165,6 +173,15 @@ class SubTask < ActiveRecord::Base multi_tenant :account belongs_to :task has_one :project, through: :task + has_many :optional_sub_tasks +end + +with_belongs_to_required_by_default do + class OptionalSubTask < ActiveRecord::Base + multi_tenant :account, optional: true + belongs_to :account, optional: true + belongs_to :sub_task + end end class StiSubTask < SubTask diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 04c43c4e..bc4ef575 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -46,4 +46,11 @@ def uses_prepared_statements? ActiveRecord::Base.connection.prepared_statements end +def with_belongs_to_required_by_default(&block) + default_value = ActiveRecord::Base.belongs_to_required_by_default + ActiveRecord::Base.belongs_to_required_by_default = true + yield +ensure + ActiveRecord::Base.belongs_to_required_by_default = default_value +end require 'schema' From 8a7b92c33501a70a866ae8c8a9cd7a276a1e51d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Thu, 19 May 2022 23:50:43 +0000 Subject: [PATCH 031/105] 2.0.0 --- CHANGELOG.md | 9 +++++++++ lib/activerecord-multi-tenant/version.rb | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ce73e0f..2b4c5e6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 2.0.0 2022-05-19 + +* Replace RequestStore with CurrentAttributes [#139](https://github.com/citusdata/activerecord-multi-tenant/pull/139) +* Support changing table_name after calling multi_tenant [#128](https://github.com/citusdata/activerecord-multi-tenant/pull/128) +* Allow to use uuid as primary key on partition table [#112](https://github.com/citusdata/activerecord-multi-tenant/pull/112) +* Support latest Rails 5.2 [#145](https://github.com/citusdata/activerecord-multi-tenant/pull/145) +* Support optional: true for belongs_to [#147](https://github.com/citusdata/activerecord-multi-tenant/pull/147) + + ## 1.2.0 2022-03-29 * Test Rails 7 & Ruby 3 diff --git a/lib/activerecord-multi-tenant/version.rb b/lib/activerecord-multi-tenant/version.rb index 9d8f6e9f..e45d00f7 100644 --- a/lib/activerecord-multi-tenant/version.rb +++ b/lib/activerecord-multi-tenant/version.rb @@ -1,3 +1,3 @@ module MultiTenant - VERSION = '1.2.0' + VERSION = '2.0.0' end From a184dd7bb084d99ac08690c16c5d1250365a17e9 Mon Sep 17 00:00:00 2001 From: Marco Montagna Date: Tue, 16 Aug 2022 17:02:37 -0700 Subject: [PATCH 032/105] Fix query building for models with missmatched partition_keys --- lib/activerecord-multi-tenant/query_rewriter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/activerecord-multi-tenant/query_rewriter.rb b/lib/activerecord-multi-tenant/query_rewriter.rb index 38f63e13..d6668183 100644 --- a/lib/activerecord-multi-tenant/query_rewriter.rb +++ b/lib/activerecord-multi-tenant/query_rewriter.rb @@ -298,7 +298,7 @@ def build_arel(*args) if !node_join.right next end - relation_right, relation_left = relations_from_node_join(node_join) + relation_left, relation_right = relations_from_node_join(node_join) next unless relation_right && relation_left From 7c891b48b825f23c3b752665cb5689cf65b217d2 Mon Sep 17 00:00:00 2001 From: Marco Montagna Date: Tue, 16 Aug 2022 18:09:56 -0700 Subject: [PATCH 033/105] Fix tests. --- .../model_extensions_spec.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/activerecord-multi-tenant/model_extensions_spec.rb b/spec/activerecord-multi-tenant/model_extensions_spec.rb index 409e30d9..64663d89 100644 --- a/spec/activerecord-multi-tenant/model_extensions_spec.rb +++ b/spec/activerecord-multi-tenant/model_extensions_spec.rb @@ -400,10 +400,10 @@ def self.name it "applies the team_id conditions in the where clause" do option1 = <<-sql.strip - SELECT "sub_tasks".* FROM "sub_tasks" INNER JOIN "tasks" ON "sub_tasks"."task_id" = "tasks"."id" AND "sub_tasks"."account_id" = "tasks"."account_id" WHERE "tasks"."project_id" = 1 AND "sub_tasks"."account_id" = 1 AND "tasks"."account_id" = 1 + 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 sql option2 = <<-sql.strip - SELECT "sub_tasks".* FROM "sub_tasks" INNER JOIN "tasks" ON "sub_tasks"."task_id" = "tasks"."id" AND "sub_tasks"."account_id" = "tasks"."account_id" WHERE "sub_tasks"."account_id" = 1 AND "tasks"."project_id" = 1 AND "tasks"."account_id" = 1 + 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 sql account1 = Account.create! name: 'Account 1' @@ -418,7 +418,7 @@ def self.name MultiTenant.without do expected_sql = <<-sql - SELECT "sub_tasks".* FROM "sub_tasks" INNER JOIN "tasks" ON "sub_tasks"."task_id" = "tasks"."id" AND "sub_tasks"."account_id" = "tasks"."account_id" WHERE "tasks"."project_id" = 1 + 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 sql project = Project.first @@ -456,7 +456,7 @@ def self.name expect(project.categories).to include(category1) expected_sql = <<-sql - SELECT "projects".* FROM "projects" INNER JOIN "project_categories" ON "project_categories"."project_id" = "projects"."id" AND "project_categories"."account_id" = "projects"."account_id" INNER JOIN "categories" ON "categories"."id" = "project_categories"."category_id" WHERE "projects"."account_id" = 1 + 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 sql expect(Project.where(account_id: 1).joins(:categories).to_sql).to eq(expected_sql.strip) @@ -490,7 +490,7 @@ def self.name MultiTenant.without do expected_sql = <<-sql - SELECT "projects"."id" AS t0_r0, "projects"."account_id" AS t0_r1, "projects"."name" AS t0_r2, "categories"."id" AS t1_r0, "categories"."name" AS t1_r1 FROM "projects" LEFT OUTER JOIN "project_categories" ON "project_categories"."project_id" = "projects"."id" AND "project_categories"."account_id" = "projects"."account_id" LEFT OUTER JOIN "categories" ON "categories"."id" = "project_categories"."category_id" WHERE "projects"."account_id" = 1 + 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 sql expect(Project.where(account_id: 1).eager_load(:categories).to_sql).to eq(expected_sql.strip) @@ -522,7 +522,7 @@ def self.name MultiTenant.without do expected_sql = <<-sql - SELECT "tasks".* FROM "tasks" INNER JOIN "projects" ON "projects"."id" = "tasks"."project_id" AND "projects"."account_id" = "tasks"."account_id" LEFT JOIN project_categories pc ON project.category_id = pc.id WHERE "tasks"."account_id" = 1 + 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 sql 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) From 6039ea4c79ed14673befe325aa6f557903057663 Mon Sep 17 00:00:00 2001 From: Marco Montagna Date: Tue, 16 Aug 2022 18:16:46 -0700 Subject: [PATCH 034/105] Fix r/l swap --- lib/activerecord-multi-tenant/query_rewriter.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/activerecord-multi-tenant/query_rewriter.rb b/lib/activerecord-multi-tenant/query_rewriter.rb index d6668183..26124d53 100644 --- a/lib/activerecord-multi-tenant/query_rewriter.rb +++ b/lib/activerecord-multi-tenant/query_rewriter.rb @@ -298,14 +298,14 @@ def build_arel(*args) if !node_join.right next end - relation_left, relation_right = relations_from_node_join(node_join) + relation_right, relation_left = relations_from_node_join(node_join) next unless relation_right && relation_left model_right = MultiTenant.multi_tenant_model_for_table(relation_left.table_name) model_left = MultiTenant.multi_tenant_model_for_table(relation_right.table_name) if model_right && model_left - join_enforcement_clause = MultiTenant::TenantJoinEnforcementClause.new(relation_left[model_left.partition_key], relation_right) + join_enforcement_clause = MultiTenant::TenantJoinEnforcementClause.new(relation_right[model_right.partition_key], relation_left) node_join.right.expr = node_join.right.expr.and(join_enforcement_clause) end end From 34cc22bc1e8f09c338859c1e44d90233b75827b8 Mon Sep 17 00:00:00 2001 From: Marco Montagna Date: Thu, 18 Aug 2022 07:25:08 -0700 Subject: [PATCH 035/105] Identify tenant even if class name is nonstandard. --- lib/activerecord-multi-tenant/model_extensions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/activerecord-multi-tenant/model_extensions.rb b/lib/activerecord-multi-tenant/model_extensions.rb index 76ed0387..a9110b49 100644 --- a/lib/activerecord-multi-tenant/model_extensions.rb +++ b/lib/activerecord-multi-tenant/model_extensions.rb @@ -3,7 +3,7 @@ module ModelExtensionsClassMethods DEFAULT_ID_FIELD = 'id'.freeze def multi_tenant(tenant_name, options = {}) - if to_s.underscore.to_sym == tenant_name + if to_s.underscore.to_sym == tenant_name || (!table_name.nil? && table_name.singularize.to_sym == tenant_name) unless MultiTenant.with_write_only_mode_enabled? # This is the tenant model itself. Workaround for https://github.com/citusdata/citus/issues/687 before_create -> do From b2d002973d2bc859f6687e77b8867d4de7501628 Mon Sep 17 00:00:00 2001 From: Marco Montagna Date: Thu, 18 Aug 2022 10:50:35 -0700 Subject: [PATCH 036/105] Add test for nonstandard tenant model name. --- .../model_extensions_spec.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/spec/activerecord-multi-tenant/model_extensions_spec.rb b/spec/activerecord-multi-tenant/model_extensions_spec.rb index 409e30d9..9dead450 100644 --- a/spec/activerecord-multi-tenant/model_extensions_spec.rb +++ b/spec/activerecord-multi-tenant/model_extensions_spec.rb @@ -70,6 +70,24 @@ it { expect(@partition_key_not_model_task.non_model_id).to be 77 } end + + describe 'Tenant model with a nonstandard class name' do + let(:account_klass) do + Class.new(ActiveRecord::Base) do + self.table_name = 'account' + def self.name + 'UserAccount' + end + + multi_tenant(:account) + end + end + it "does not register the tenant model" do + expect(MultiTenant).not_to receive(:register_multi_tenant_model) + account_klass + end + end + describe 'Changes table_name after multi_tenant called' do before do account_klass.has_many(:posts, anonymous_class: post_klass) From a566c9f9dff8f6a87cb745d2c617b6f1603fc9ed Mon Sep 17 00:00:00 2001 From: Hasan Kumar Date: Sun, 25 Sep 2022 05:21:19 +0530 Subject: [PATCH 037/105] Make create_distributed_table, create_reference_table reversible & add ruby wrapper for rebalance_table_shards --- lib/activerecord-multi-tenant/migrations.rb | 32 +++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/lib/activerecord-multi-tenant/migrations.rb b/lib/activerecord-multi-tenant/migrations.rb index 25a4ea25..cea73762 100644 --- a/lib/activerecord-multi-tenant/migrations.rb +++ b/lib/activerecord-multi-tenant/migrations.rb @@ -2,12 +2,40 @@ module MultiTenant module MigrationExtensions def create_distributed_table(table_name, partition_key) return unless citus_version.present? - execute "SELECT create_distributed_table($$#{table_name}$$, $$#{partition_key}$$)" + + reversible do |dir| + dir.up do + execute "SELECT create_distributed_table($$#{table_name}$$, $$#{partition_key}$$)" + end + dir.down do + undistribute_table(table_name) + end + end end def create_reference_table(table_name) return unless citus_version.present? - execute "SELECT create_reference_table($$#{table_name}$$)" + + reversible do |dir| + dir.up do + execute "SELECT create_reference_table($$#{table_name}$$)" + end + dir.down do + undistribute_table(table_name) + end + end + end + + def undistribute_table(table_name) + return unless citus_version.present? + + execute "SELECT undistribute_table($$#{table_name}$$))" + end + + def rebalance_table_shards + return unless citus_version.present? + + execute 'SELECT rebalance_table_shards()' end def execute_on_all_nodes(sql) From a3cfc5f00db80ca376c646b21ab25eb4a27bd325 Mon Sep 17 00:00:00 2001 From: Hasan Kumar Date: Sun, 25 Sep 2022 04:00:57 +0530 Subject: [PATCH 038/105] Add current_tenant_id to WHERE clauses when calling methods on activerecord instance or its associations --- lib/activerecord-multi-tenant.rb | 1 - .../model_extensions.rb | 14 +++++++++++++ lib/activerecord-multi-tenant/multi_tenant.rb | 17 +++++++++++++++ .../persistence_extension.rb | 13 ------------ .../associations_spec.rb | 21 +++++++++++++++++++ .../model_extensions_spec.rb | 6 +----- .../record_modifications_spec.rb | 19 +++++++++++++++++ spec/schema.rb | 1 - 8 files changed, 72 insertions(+), 20 deletions(-) delete mode 100644 lib/activerecord-multi-tenant/persistence_extension.rb create mode 100644 spec/activerecord-multi-tenant/associations_spec.rb diff --git a/lib/activerecord-multi-tenant.rb b/lib/activerecord-multi-tenant.rb index f1e306a1..760126ed 100644 --- a/lib/activerecord-multi-tenant.rb +++ b/lib/activerecord-multi-tenant.rb @@ -10,4 +10,3 @@ require_relative 'activerecord-multi-tenant/query_monitor' require_relative 'activerecord-multi-tenant/version' require_relative 'activerecord-multi-tenant/with_lock' -require_relative 'activerecord-multi-tenant/persistence_extension' diff --git a/lib/activerecord-multi-tenant/model_extensions.rb b/lib/activerecord-multi-tenant/model_extensions.rb index a9110b49..e4bb9d00 100644 --- a/lib/activerecord-multi-tenant/model_extensions.rb +++ b/lib/activerecord-multi-tenant/model_extensions.rb @@ -129,6 +129,20 @@ def inherited(subclass) ActiveSupport.on_load(:active_record) do |base| base.extend MultiTenant::ModelExtensionsClassMethods + + # Ensure we have current_tenant_id in where clause when a cached ActiveRecord instance is being reloaded, or update_columns without callbacks is called + MultiTenant.wrap_methods(ActiveRecord::Base, 'self', :delete, :reload, :update_columns) + + # Any queuries fired for fetching a singular association have the correct current_tenant_id in WHERE clause + # reload is called anytime any record's association is accessed + MultiTenant.wrap_methods(ActiveRecord::Associations::Association, 'owner', :reload) + + # 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 + ActiveRecord::Associations::CollectionProxy.alias_method :equals_mt, :== # Hack to prevent syntax error due to invalid method name + ActiveRecord::Associations::CollectionProxy.alias_method :append_mt, :<< # Hack to prevent syntax error due to invalid method name + 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) + ActiveRecord::Associations::CollectionProxy.alias_method :==, :equals_mt + ActiveRecord::Associations::CollectionProxy.alias_method :<<, :append_mt end class ActiveRecord::Associations::Association diff --git a/lib/activerecord-multi-tenant/multi_tenant.rb b/lib/activerecord-multi-tenant/multi_tenant.rb index d7412325..f7cbce3b 100644 --- a/lib/activerecord-multi-tenant/multi_tenant.rb +++ b/lib/activerecord-multi-tenant/multi_tenant.rb @@ -109,6 +109,23 @@ def self.without(&block) end end + # 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 + def self.wrap_methods(klass, owner, *method_names) + method_names.each do |method_name| + original_method_name = :"_mt_original_#{method_name}" + klass.class_eval <<-CODE, __FILE__, __LINE__ + 1 + alias_method :#{original_method_name}, :#{method_name} + def #{method_name}(*args, &block) + if MultiTenant.multi_tenant_model_for_table(#{owner}.class.table_name).present? && #{owner}.persisted? && MultiTenant.current_tenant_id.nil? + MultiTenant.with(#{owner}.public_send(#{owner}.class.partition_key)) { #{original_method_name}(*args) } + else + #{original_method_name}(*args) + end + end + CODE + end + end + # Preserve backward compatibility for people using .with_id singleton_class.send(:alias_method, :with_id, :with) diff --git a/lib/activerecord-multi-tenant/persistence_extension.rb b/lib/activerecord-multi-tenant/persistence_extension.rb deleted file mode 100644 index e42e87c2..00000000 --- a/lib/activerecord-multi-tenant/persistence_extension.rb +++ /dev/null @@ -1,13 +0,0 @@ -module ActiveRecord - module Persistence - alias :delete_orig :delete - - def delete - if MultiTenant.multi_tenant_model_for_table(self.class.table_name).present? && persisted? && MultiTenant.current_tenant_id.nil? - MultiTenant.with(self.public_send(self.class.partition_key)) { delete_orig } - else - delete_orig - end - end - end -end diff --git a/spec/activerecord-multi-tenant/associations_spec.rb b/spec/activerecord-multi-tenant/associations_spec.rb new file mode 100644 index 00000000..017aa119 --- /dev/null +++ b/spec/activerecord-multi-tenant/associations_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe MultiTenant, 'Association methods' do + let(:account1) { Account.create! name: 'test1' } + let(:account2) { Account.create! name: 'test2' } + let(:project1) { Project.create! name: 'something1', account: account1 } + let(:project2) { Project.create! name: 'something2', account: account2, id: project1.id } + let(:task1) { Task.create! name: 'task1', project: project1, account: account1 } + let(:task2) { Task.create! name: 'task2', project: project2, account: account2, id: task1.id } + + context 'include the tenant_id in queries and' do + it 'creates a task with correct account_id' do + expect(project2.tasks.create(name: 'task3').account_id).to eq(account2.id) + end + it 'return correct account_id' do + expect(task1.project.account_id).to_not eq(task2.project.account_id) # belongs_to + expect(project2.tasks.count).to eq(1) + expect(project2.tasks.first.account_id).to eq(account2.id) # has_many + end + end +end diff --git a/spec/activerecord-multi-tenant/model_extensions_spec.rb b/spec/activerecord-multi-tenant/model_extensions_spec.rb index fc7fa39a..f126cffb 100644 --- a/spec/activerecord-multi-tenant/model_extensions_spec.rb +++ b/spec/activerecord-multi-tenant/model_extensions_spec.rb @@ -204,11 +204,7 @@ def self.name end it 'handles belongs_to with optional: true' do - MultiTenant.with(account) do - sub_task - end - - record = sub_task.optional_sub_tasks.create! + record = OptionalSubTask.create(sub_task_id: sub_task.id) expect(record.reload.sub_task).to eq(sub_task) expect(record.account_id).to eq(nil) end diff --git a/spec/activerecord-multi-tenant/record_modifications_spec.rb b/spec/activerecord-multi-tenant/record_modifications_spec.rb index 76376177..9e0dcd1c 100644 --- a/spec/activerecord-multi-tenant/record_modifications_spec.rb +++ b/spec/activerecord-multi-tenant/record_modifications_spec.rb @@ -54,6 +54,25 @@ end end + it 'should not update other objects with same id when calling object.update_columns' do + # When two records with same id but different account_id are updated, it should only update the current one + expect(project.account).to eq(account) + expect(project2.account).to eq(account2) + expect(project.id).to eq(project2.id) + + MultiTenant.without do + project2.update_columns(name: 'newthing2') + expect(project.reload.name).to eq('something') + expect(project2.reload.name).to eq('newthing2') + end + end + + it 'should return the same object when calling object.reload' do + # When two records with same id but different account_id are updated, it should not return the other object + expect(project.reload.account_id).to eq(account.id) + expect(project2.reload.account_id).to eq(account2.id) + end + it 'test delete for reference tables' do category1 = Category.create! name: 'Category 1' expect(Category.count).to eq(1) diff --git a/spec/schema.rb b/spec/schema.rb index dbb515f3..ed7d0eec 100644 --- a/spec/schema.rb +++ b/spec/schema.rb @@ -179,7 +179,6 @@ class SubTask < ActiveRecord::Base with_belongs_to_required_by_default do class OptionalSubTask < ActiveRecord::Base multi_tenant :account, optional: true - belongs_to :account, optional: true belongs_to :sub_task end end From 0c2cd7170bb859ccf9d3d4d6629066e76731cc19 Mon Sep 17 00:00:00 2001 From: Hasan Kumar Date: Mon, 26 Sep 2022 15:13:25 +0530 Subject: [PATCH 039/105] Support create_{distributed|reference}_table in schema.rb generated from rake db:schema:dump --- lib/activerecord-multi-tenant/migrations.rb | 45 ++++++++++++++++++--- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/lib/activerecord-multi-tenant/migrations.rb b/lib/activerecord-multi-tenant/migrations.rb index cea73762..b9c0b3d9 100644 --- a/lib/activerecord-multi-tenant/migrations.rb +++ b/lib/activerecord-multi-tenant/migrations.rb @@ -56,21 +56,19 @@ def enable_extension_on_all_nodes(extension) end def citus_version - execute("SELECT extversion FROM pg_extension WHERE extname = 'citus'").getvalue(0,0).try(:split, '-').try(:first) + execute("SELECT extversion FROM pg_extension WHERE extname = 'citus'").getvalue(0, 0).try(:split, '-').try(:first) rescue ArgumentError => e - raise unless e.message == "invalid tuple number 0" + raise unless e.message == 'invalid tuple number 0' end end end -if defined?(ActiveRecord::Migration) - ActiveRecord::Migration.send(:include, MultiTenant::MigrationExtensions) -end +ActiveRecord::Migration.include MultiTenant::MigrationExtensions if defined?(ActiveRecord::Migration) module ActiveRecord module ConnectionAdapters # :nodoc: module SchemaStatements - alias :orig_create_table :create_table + alias orig_create_table create_table def create_table(table_name, options = {}, &block) ret = orig_create_table(table_name, **options.except(:partition_key), &block) if options[:partition_key] && options[:partition_key].to_s != 'id' @@ -82,3 +80,38 @@ def create_table(table_name, options = {}, &block) end end end + +module ActiveRecord + class SchemaDumper + private + + alias initialize_without_citus initialize + def initialize(connection, options = {}) + initialize_without_citus(connection, options) + + @distribution_columns = + if ActiveRecord::Migration.citus_version.present? + @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| + [v['table_name'], v['dist_col_name']] + end + else + {} + end + end + + # Support for create_distributed_table & create_reference_table + alias table_without_citus table + def table(table, stream) + table_without_citus(table, stream) + table_name = remove_prefix_and_suffix(table) + distribution_column = @distribution_columns[table_name] + if distribution_column + stream.puts " create_distributed_table(#{table_name.inspect}, #{distribution_column.inspect})" + stream.puts + elsif @distribution_columns.key?(table_name) + stream.puts " create_reference_table(#{table_name.inspect})" + stream.puts + end + end + end +end From 1fea0828c9569f555e008c11c566edf408ba287f Mon Sep 17 00:00:00 2001 From: Shayon Mukherjee Date: Wed, 19 Oct 2022 09:33:44 -0400 Subject: [PATCH 040/105] Add client and server sidekiq middleware to sidekiq middleware chain Today we don't have any public documentation for the gem on how/where to add the Sidekiq middlewares to ensure that `multi_tenant` object is attached to the Sidekiq job when enqueuing. It can take bit of figuring out to do it the right away. Instead, with this PR, the experience can be a bit nicer because as a consumer of this gem we won't have to think about how/where to add this. All we will need is a require `'activerecord-multi-tenant/sidekiq'` Without the addition of middleware, the `multi_tenant` object doesn't get attached to the job --- lib/activerecord-multi-tenant/sidekiq.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/activerecord-multi-tenant/sidekiq.rb b/lib/activerecord-multi-tenant/sidekiq.rb index 4be568ad..4ad51bc5 100644 --- a/lib/activerecord-multi-tenant/sidekiq.rb +++ b/lib/activerecord-multi-tenant/sidekiq.rb @@ -33,6 +33,23 @@ def call(worker_class, msg, queue) end end +if defined?(Sidekiq) + Sidekiq.configure_server do |config| + config.server_middleware do |chain| + chain.add Sidekiq::Middleware::MultiTenant::Server + end + config.client_middleware do |chain| + chain.add Sidekiq::Middleware::MultiTenant::Client + end + end + + Sidekiq.configure_client do |config| + config.client_middleware do |chain| + chain.add Sidekiq::Middleware::MultiTenant::Client + end + end +end + module Sidekiq class Client def push_bulk_with_tenants(items) From e2cc8c0c6c91bce39c911d6b4941ec5d418102ec Mon Sep 17 00:00:00 2001 From: Shayon Mukherjee Date: Wed, 19 Oct 2022 17:15:00 -0400 Subject: [PATCH 041/105] Remove defined? call Its redundant --- lib/activerecord-multi-tenant/sidekiq.rb | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/lib/activerecord-multi-tenant/sidekiq.rb b/lib/activerecord-multi-tenant/sidekiq.rb index 4ad51bc5..40cd9484 100644 --- a/lib/activerecord-multi-tenant/sidekiq.rb +++ b/lib/activerecord-multi-tenant/sidekiq.rb @@ -33,23 +33,22 @@ def call(worker_class, msg, queue) end end -if defined?(Sidekiq) - Sidekiq.configure_server do |config| - config.server_middleware do |chain| - chain.add Sidekiq::Middleware::MultiTenant::Server - end - config.client_middleware do |chain| - chain.add Sidekiq::Middleware::MultiTenant::Client - end +Sidekiq.configure_server do |config| + config.server_middleware do |chain| + chain.add Sidekiq::Middleware::MultiTenant::Server + end + config.client_middleware do |chain| + chain.add Sidekiq::Middleware::MultiTenant::Client end +end - Sidekiq.configure_client do |config| - config.client_middleware do |chain| - chain.add Sidekiq::Middleware::MultiTenant::Client - end +Sidekiq.configure_client do |config| + config.client_middleware do |chain| + chain.add Sidekiq::Middleware::MultiTenant::Client end end + module Sidekiq class Client def push_bulk_with_tenants(items) From 0cf100ff69d0066cc7b914771a91fa835398d31d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Thu, 20 Oct 2022 15:21:00 +0000 Subject: [PATCH 042/105] 2.1.1 --- CHANGELOG.md | 8 ++++++++ lib/activerecord-multi-tenant/version.rb | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b4c5e6c..197799b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 2.1.0 2022-10-20 +* Fix query building for models with mismatched partition_keys [#150](https://github.com/citusdata/activerecord-multi-tenant/pull/150) +* Identify tenant even if class name is nonstandard [#152](https://github.com/citusdata/activerecord-multi-tenant/pull/152) +* 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) +* 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) +* Support create_distributed_table, create_reference_table in schema.rb [#156](https://github.com/citusdata/activerecord-multi-tenant/pull/156) +* Add client and server sidekiq middleware to sidekiq middleware chain [#158](https://github.com/citusdata/activerecord-multi-tenant/pull/158) + ## 2.0.0 2022-05-19 * Replace RequestStore with CurrentAttributes [#139](https://github.com/citusdata/activerecord-multi-tenant/pull/139) diff --git a/lib/activerecord-multi-tenant/version.rb b/lib/activerecord-multi-tenant/version.rb index e45d00f7..25aa3caf 100644 --- a/lib/activerecord-multi-tenant/version.rb +++ b/lib/activerecord-multi-tenant/version.rb @@ -1,3 +1,3 @@ module MultiTenant - VERSION = '2.0.0' + VERSION = '2.1.1' end From 0e9a2e25131e3d6a46380fbf5806bf8b6ab003df Mon Sep 17 00:00:00 2001 From: Daniel Naves de Carvalho Date: Wed, 26 Oct 2022 08:06:03 -0300 Subject: [PATCH 043/105] Fixes issue when wraping methods that require a block This fixes the issue introduced by https://github.com/citusdata/activerecord-multi-tenant/pull/154 --- lib/activerecord-multi-tenant/multi_tenant.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/activerecord-multi-tenant/multi_tenant.rb b/lib/activerecord-multi-tenant/multi_tenant.rb index f7cbce3b..70957bc6 100644 --- a/lib/activerecord-multi-tenant/multi_tenant.rb +++ b/lib/activerecord-multi-tenant/multi_tenant.rb @@ -117,9 +117,9 @@ def self.wrap_methods(klass, owner, *method_names) alias_method :#{original_method_name}, :#{method_name} def #{method_name}(*args, &block) if MultiTenant.multi_tenant_model_for_table(#{owner}.class.table_name).present? && #{owner}.persisted? && MultiTenant.current_tenant_id.nil? - MultiTenant.with(#{owner}.public_send(#{owner}.class.partition_key)) { #{original_method_name}(*args) } + MultiTenant.with(#{owner}.public_send(#{owner}.class.partition_key)) { #{original_method_name}(*args, &block) } else - #{original_method_name}(*args) + #{original_method_name}(*args, &block) end end CODE From 901540f9145129173601e674c2afe10fdeb3f31a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Wed, 26 Oct 2022 19:20:59 +0000 Subject: [PATCH 044/105] 2.1.2 --- CHANGELOG.md | 5 ++++- lib/activerecord-multi-tenant/version.rb | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 197799b0..5cc92dec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog -## 2.1.0 2022-10-20 +## 2.1.2 2022-10-26 +* Fixes issue when wraping methods that require a block [#162](https://github.com/citusdata/activerecord-multi-tenant/pull/162) + +## 2.1.1 2022-10-20 * Fix query building for models with mismatched partition_keys [#150](https://github.com/citusdata/activerecord-multi-tenant/pull/150) * Identify tenant even if class name is nonstandard [#152](https://github.com/citusdata/activerecord-multi-tenant/pull/152) * 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) diff --git a/lib/activerecord-multi-tenant/version.rb b/lib/activerecord-multi-tenant/version.rb index 25aa3caf..a8c80d2b 100644 --- a/lib/activerecord-multi-tenant/version.rb +++ b/lib/activerecord-multi-tenant/version.rb @@ -1,3 +1,3 @@ module MultiTenant - VERSION = '2.1.1' + VERSION = '2.1.2' end From 90043ad3dd02807ee6e319dceb27e0b5d394adc5 Mon Sep 17 00:00:00 2001 From: manga_osyo Date: Thu, 27 Oct 2022 22:33:49 +0900 Subject: [PATCH 045/105] Fix error when calling methods that take keyword arguments with M`ultiTenant.wrap_methods`. --- lib/activerecord-multi-tenant/multi_tenant.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/activerecord-multi-tenant/multi_tenant.rb b/lib/activerecord-multi-tenant/multi_tenant.rb index 70957bc6..a5294d3d 100644 --- a/lib/activerecord-multi-tenant/multi_tenant.rb +++ b/lib/activerecord-multi-tenant/multi_tenant.rb @@ -115,11 +115,11 @@ def self.wrap_methods(klass, owner, *method_names) original_method_name = :"_mt_original_#{method_name}" klass.class_eval <<-CODE, __FILE__, __LINE__ + 1 alias_method :#{original_method_name}, :#{method_name} - def #{method_name}(*args, &block) + def #{method_name}(...) if MultiTenant.multi_tenant_model_for_table(#{owner}.class.table_name).present? && #{owner}.persisted? && MultiTenant.current_tenant_id.nil? - MultiTenant.with(#{owner}.public_send(#{owner}.class.partition_key)) { #{original_method_name}(*args, &block) } + MultiTenant.with(#{owner}.public_send(#{owner}.class.partition_key)) { #{original_method_name}(...) } else - #{original_method_name}(*args, &block) + #{original_method_name}(...) end end CODE From ed6a1495ad9e723c50c5342ed12e678ad9316314 Mon Sep 17 00:00:00 2001 From: manga_osyo Date: Thu, 27 Oct 2022 22:51:39 +0900 Subject: [PATCH 046/105] Add support Ruby 2.6 or lower. --- lib/activerecord-multi-tenant/multi_tenant.rb | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/lib/activerecord-multi-tenant/multi_tenant.rb b/lib/activerecord-multi-tenant/multi_tenant.rb index a5294d3d..b06080ef 100644 --- a/lib/activerecord-multi-tenant/multi_tenant.rb +++ b/lib/activerecord-multi-tenant/multi_tenant.rb @@ -110,10 +110,27 @@ def self.without(&block) end # 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 - def self.wrap_methods(klass, owner, *method_names) - method_names.each do |method_name| - original_method_name = :"_mt_original_#{method_name}" - klass.class_eval <<-CODE, __FILE__, __LINE__ + 1 + if Gem::Version.create(RUBY_VERSION) < Gem::Version.new("2.7.0") + def self.wrap_methods(klass, owner, *method_names) + method_names.each do |method_name| + original_method_name = :"_mt_original_#{method_name}" + klass.class_eval <<-CODE, __FILE__, __LINE__ + 1 + alias_method :#{original_method_name}, :#{method_name} + def #{method_name}(*args, &block) + if MultiTenant.multi_tenant_model_for_table(#{owner}.class.table_name).present? && #{owner}.persisted? && MultiTenant.current_tenant_id.nil? + MultiTenant.with(#{owner}.public_send(#{owner}.class.partition_key)) { #{original_method_name}(*args, &block) } + else + #{original_method_name}(*args, &block) + end + end + CODE + end + end + else + def self.wrap_methods(klass, owner, *method_names) + method_names.each do |method_name| + original_method_name = :"_mt_original_#{method_name}" + klass.class_eval <<-CODE, __FILE__, __LINE__ + 1 alias_method :#{original_method_name}, :#{method_name} def #{method_name}(...) if MultiTenant.multi_tenant_model_for_table(#{owner}.class.table_name).present? && #{owner}.persisted? && MultiTenant.current_tenant_id.nil? @@ -122,7 +139,8 @@ def #{method_name}(...) #{original_method_name}(...) end end - CODE + CODE + end end end From d7481d64b85abf9e5d6f81a0cb2ad101fe765902 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Thu, 27 Oct 2022 14:40:52 +0000 Subject: [PATCH 047/105] 2.1.3 --- CHANGELOG.md | 3 +++ lib/activerecord-multi-tenant/version.rb | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cc92dec..45a62f28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 2.1.3 2022-10-27 +* Error when calling a method that takes keyword arguments with MultiTenant.wrap_methods [#164](https://github.com/citusdata/activerecord-multi-tenant/pull/164) + ## 2.1.2 2022-10-26 * Fixes issue when wraping methods that require a block [#162](https://github.com/citusdata/activerecord-multi-tenant/pull/162) diff --git a/lib/activerecord-multi-tenant/version.rb b/lib/activerecord-multi-tenant/version.rb index a8c80d2b..21b1c637 100644 --- a/lib/activerecord-multi-tenant/version.rb +++ b/lib/activerecord-multi-tenant/version.rb @@ -1,3 +1,3 @@ module MultiTenant - VERSION = '2.1.2' + VERSION = '2.1.3' end From 497f11e060ffe0739bdd50154622bcaebd4596b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Thu, 27 Oct 2022 15:24:41 +0000 Subject: [PATCH 048/105] Update README Rail version details --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cd9cbae1..822ecfe5 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ gem 'activerecord-multi-tenant' ## Supported Rails versions -All Ruby on Rails versions starting with 4.2 or newer (up to 6.0) are supported. +All Ruby on Rails versions starting with 5.2 or newer (up to 7.0) are supported. This gem only supports ActiveRecord (the Rails default ORM), and not alternative ORMs like Sequel. From 1b12d53cce66231160c91a734c5a71d785985151 Mon Sep 17 00:00:00 2001 From: Hasan Kumar Date: Thu, 3 Nov 2022 11:41:22 +0530 Subject: [PATCH 049/105] Fixes #166 where db:schema:dump is broken when using this gem with MySQL --- lib/activerecord-multi-tenant/migrations.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/activerecord-multi-tenant/migrations.rb b/lib/activerecord-multi-tenant/migrations.rb index b9c0b3d9..75599bec 100644 --- a/lib/activerecord-multi-tenant/migrations.rb +++ b/lib/activerecord-multi-tenant/migrations.rb @@ -89,8 +89,14 @@ class SchemaDumper def initialize(connection, options = {}) initialize_without_citus(connection, options) + citus_version = begin + ActiveRecord::Migration.citus_version + rescue StandardError + # Handle the case where this gem is used with MySQL https://github.com/citusdata/activerecord-multi-tenant/issues/166 + nil + end @distribution_columns = - if ActiveRecord::Migration.citus_version.present? + if citus_version.present? @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| [v['table_name'], v['dist_col_name']] end From 1c1def9134b2b050593ac91ac11107b688af20fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Thu, 3 Nov 2022 14:39:28 +0000 Subject: [PATCH 050/105] 2.1.4 --- CHANGELOG.md | 3 +++ lib/activerecord-multi-tenant/version.rb | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45a62f28..b295c430 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 2.1.4 2022-11-03 +* Fixes #166 where db:schema:dump is broken when using this gem with MySQL [#167](https://github.com/citusdata/activerecord-multi-tenant/pull/167) + ## 2.1.3 2022-10-27 * Error when calling a method that takes keyword arguments with MultiTenant.wrap_methods [#164](https://github.com/citusdata/activerecord-multi-tenant/pull/164) diff --git a/lib/activerecord-multi-tenant/version.rb b/lib/activerecord-multi-tenant/version.rb index 21b1c637..0f4940f8 100644 --- a/lib/activerecord-multi-tenant/version.rb +++ b/lib/activerecord-multi-tenant/version.rb @@ -1,3 +1,3 @@ module MultiTenant - VERSION = '2.1.3' + VERSION = '2.1.4' end From 202a614efde7941d2c6ce8d0807faed081e9690d Mon Sep 17 00:00:00 2001 From: harsheetjain <=> Date: Thu, 17 Nov 2022 14:35:05 +0530 Subject: [PATCH 051/105] Include test cases for joins --- .../model_extensions_spec.rb | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/spec/activerecord-multi-tenant/model_extensions_spec.rb b/spec/activerecord-multi-tenant/model_extensions_spec.rb index f126cffb..41cc6a98 100644 --- a/spec/activerecord-multi-tenant/model_extensions_spec.rb +++ b/spec/activerecord-multi-tenant/model_extensions_spec.rb @@ -299,6 +299,55 @@ def self.name end end + # Joins + describe 'joins for models' do + context 'for models with where condition in associations' do + let(:account) { Account.create!(name: 'Account 1') } + + it 'should add tenant condition to the queries when tenant is set' do + expected_join_sql = <<-SQL.strip + 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 + SQL + + MultiTenant.with(account) do + expect(Comment.joins(:task).to_sql).to eq(expected_join_sql) + end + end + + it 'should add tenant condition to the queries when tenant is not set' do + MultiTenant.without do + expected_join_sql = <<-SQL.strip + 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" + SQL + expect(Comment.joins(:task).to_sql).to eq(expected_join_sql) + end + end + end + + context 'for models with default associations' do + let(:account) { Account.create!(name: 'Account 1') } + + it 'should add tenant condition to the queries when tenant is set' do + expected_join_sql = <<-SQL.strip + SELECT "projects".* FROM "projects" INNER JOIN "tasks" ON "tasks"."project_id" = "projects"."id" AND "tasks"."account_id" = 1 WHERE "projects"."account_id" = 1 + SQL + + MultiTenant.with(account) do + expect(Project.joins(:tasks).to_sql).to eq(expected_join_sql) + end + end + + it 'should add tenant condition to the queries when tenant is not set' do + MultiTenant.without do + expected_join_sql = <<-SQL.strip + SELECT "projects".* FROM "projects" INNER JOIN "tasks" ON "tasks"."project_id" = "projects"."id" AND "projects"."account_id" = "tasks"."account_id" + SQL + expect(Project.joins(:tasks).to_sql).to eq(expected_join_sql) + end + end + end + end + # ::with describe "::with" do it "should set current_tenant to the specified tenant inside the block" do From 88aa29a07a92bdedb7e4b61353b7303a1d741c39 Mon Sep 17 00:00:00 2001 From: harsheetjain <=> Date: Fri, 18 Nov 2022 13:26:42 +0530 Subject: [PATCH 052/105] resolving nested children Description rails is further adding a children node --- lib/activerecord-multi-tenant/query_rewriter.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/activerecord-multi-tenant/query_rewriter.rb b/lib/activerecord-multi-tenant/query_rewriter.rb index 26124d53..ab9e039d 100644 --- a/lib/activerecord-multi-tenant/query_rewriter.rb +++ b/lib/activerecord-multi-tenant/query_rewriter.rb @@ -330,8 +330,9 @@ def relations_from_node_join(node_join) return nil, nil end - if children[0].right.respond_to?('relation') && children[0].left.respond_to?('relation') - return children[0].right.relation, children[0].left.relation + child = children.first.respond_to?(:children) ? children.first.children.first : children.first + if child.right.respond_to?(:relation) && child.left.respond_to?(:relation) + return child.right.relation, child.left.relation end return nil, nil From 81046218b168bb240d9b8fcd58b4f529dc02b439 Mon Sep 17 00:00:00 2001 From: harsheetjain <=> Date: Sun, 20 Nov 2022 13:04:06 +0530 Subject: [PATCH 053/105] drop support for rails 5.2.3 --- .github/workflows/CI.yml | 10 ---------- gemfiles/active_record_5.2.3.gemfile | 16 ---------------- gemfiles/rails_5.2.3.gemfile | 16 ---------------- 3 files changed, 42 deletions(-) delete mode 100644 gemfiles/active_record_5.2.3.gemfile delete mode 100644 gemfiles/rails_5.2.3.gemfile diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index dc729358..d22b6b67 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -19,12 +19,10 @@ jobs: - '3.0' - '3.1' gemfile: - - rails_5.2.3 - rails_5.2 - rails_6.0 - rails_6.1 - rails_7.0 - - active_record_5.2.3 - active_record_5.2 - active_record_6.0 - active_record_6.1 @@ -49,14 +47,6 @@ jobs: gemfile: 'rails_5.2' - ruby: '3.1' gemfile: 'active_record_5.2' - - ruby: '3.0' - gemfile: 'rails_5.2.3' - - ruby: '3.0' - gemfile: 'active_record_5.2.3' - - ruby: '3.1' - gemfile: 'rails_5.2.3' - - ruby: '3.1' - gemfile: 'active_record_5.2.3' name: Ruby ${{ matrix.ruby }} / ${{ matrix.gemfile }} ${{ (matrix.prepared_statements && 'w/ prepared statements') || '' }} env: BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile diff --git a/gemfiles/active_record_5.2.3.gemfile b/gemfiles/active_record_5.2.3.gemfile deleted file mode 100644 index 59d69767..00000000 --- a/gemfiles/active_record_5.2.3.gemfile +++ /dev/null @@ -1,16 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "appraisal" -gem "activerecord", "~> 5.2.0", "< 5.2.4" # FIXME -gem "i18n", "~> 0.9.5" -gem "nokogiri", "~> 1.7.1" -gem "nio4r", "~> 2.3.1" -gem "sprockets", "~> 3.7.1" -gem "byebug", "~> 11.0" -gem "rake", "12.0.0" -gem "redis", "3.3.3" -gem "pry-byebug", "3.9.0" - -gemspec path: "../" diff --git a/gemfiles/rails_5.2.3.gemfile b/gemfiles/rails_5.2.3.gemfile deleted file mode 100644 index 5c24f073..00000000 --- a/gemfiles/rails_5.2.3.gemfile +++ /dev/null @@ -1,16 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "appraisal" -gem "rails", "~> 5.2.0", "< 5.2.4" # FIXME -gem "i18n", "~> 0.9.5" -gem "nokogiri", "~> 1.7.1" -gem "nio4r", "~> 2.3.1" -gem "sprockets", "~> 3.7.1" -gem "byebug", "~> 11.0" -gem "rake", "12.0.0" -gem "redis", "3.3.3" -gem "pry-byebug", "3.9.0" - -gemspec path: "../" From a82fd750fc815e36cdd7bbb69abdd0eb94d9dcf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Sun, 20 Nov 2022 21:33:14 +0000 Subject: [PATCH 054/105] 2.1.5 --- CHANGELOG.md | 3 +++ lib/activerecord-multi-tenant/version.rb | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b295c430..4c4982dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 2.1.5 2022-11-20 +* Fix `MultiTenant.without` codegen bug in Rails 6.1+ [#168](https://github.com/citusdata/activerecord-multi-tenant/pull/168) + ## 2.1.4 2022-11-03 * Fixes #166 where db:schema:dump is broken when using this gem with MySQL [#167](https://github.com/citusdata/activerecord-multi-tenant/pull/167) diff --git a/lib/activerecord-multi-tenant/version.rb b/lib/activerecord-multi-tenant/version.rb index 0f4940f8..44c8d469 100644 --- a/lib/activerecord-multi-tenant/version.rb +++ b/lib/activerecord-multi-tenant/version.rb @@ -1,3 +1,3 @@ module MultiTenant - VERSION = '2.1.4' + VERSION = '2.1.5' end From fadba86f74fe6abe600275fe12d6edb0fdaf52a5 Mon Sep 17 00:00:00 2001 From: harsheetjain Date: Wed, 23 Nov 2022 14:36:21 +0530 Subject: [PATCH 055/105] updated check for ruby version --- lib/activerecord-multi-tenant/model_extensions.rb | 2 ++ lib/activerecord-multi-tenant/multi_tenant.rb | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/activerecord-multi-tenant/model_extensions.rb b/lib/activerecord-multi-tenant/model_extensions.rb index e4bb9d00..1cfa75b2 100644 --- a/lib/activerecord-multi-tenant/model_extensions.rb +++ b/lib/activerecord-multi-tenant/model_extensions.rb @@ -1,3 +1,5 @@ +require_relative './multi_tenant' + module MultiTenant module ModelExtensionsClassMethods DEFAULT_ID_FIELD = 'id'.freeze diff --git a/lib/activerecord-multi-tenant/multi_tenant.rb b/lib/activerecord-multi-tenant/multi_tenant.rb index b06080ef..4769035d 100644 --- a/lib/activerecord-multi-tenant/multi_tenant.rb +++ b/lib/activerecord-multi-tenant/multi_tenant.rb @@ -110,7 +110,7 @@ def self.without(&block) end # 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 - if Gem::Version.create(RUBY_VERSION) < Gem::Version.new("2.7.0") + if Gem::Version.create(RUBY_VERSION) < Gem::Version.new('3.0.0') def self.wrap_methods(klass, owner, *method_names) method_names.each do |method_name| original_method_name = :"_mt_original_#{method_name}" From 38ba34f974830ebdedcfde8291fd5a408fca9397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Wed, 23 Nov 2022 13:06:46 +0000 Subject: [PATCH 056/105] 2.1.6 --- CHANGELOG.md | 3 +++ lib/activerecord-multi-tenant/version.rb | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c4982dd..961a5086 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 2.1.6 2022-11-23 +* Fix undefined wrap_methods error & wrap_methods version check [#170](https://github.com/citusdata/activerecord-multi-tenant/pull/170) + ## 2.1.5 2022-11-20 * Fix `MultiTenant.without` codegen bug in Rails 6.1+ [#168](https://github.com/citusdata/activerecord-multi-tenant/pull/168) diff --git a/lib/activerecord-multi-tenant/version.rb b/lib/activerecord-multi-tenant/version.rb index 44c8d469..211e3da5 100644 --- a/lib/activerecord-multi-tenant/version.rb +++ b/lib/activerecord-multi-tenant/version.rb @@ -1,3 +1,3 @@ module MultiTenant - VERSION = '2.1.5' + VERSION = '2.1.' end From 2684eb08fbdcb7948cf7a0cb07e11b61183a4e40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Wed, 23 Nov 2022 13:07:21 +0000 Subject: [PATCH 057/105] Fix version string --- lib/activerecord-multi-tenant/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/activerecord-multi-tenant/version.rb b/lib/activerecord-multi-tenant/version.rb index 211e3da5..a0a5bce4 100644 --- a/lib/activerecord-multi-tenant/version.rb +++ b/lib/activerecord-multi-tenant/version.rb @@ -1,3 +1,3 @@ module MultiTenant - VERSION = '2.1.' + VERSION = '2.1.6' end From 9ec1194aea480da9ae9b706393091ad5bf252dcb Mon Sep 17 00:00:00 2001 From: Daniel Naves de Carvalho Date: Sat, 26 Nov 2022 21:11:40 -0300 Subject: [PATCH 058/105] Allow Partitioned tables to be created without a primary key --- lib/activerecord-multi-tenant/migrations.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/activerecord-multi-tenant/migrations.rb b/lib/activerecord-multi-tenant/migrations.rb index 75599bec..cda58112 100644 --- a/lib/activerecord-multi-tenant/migrations.rb +++ b/lib/activerecord-multi-tenant/migrations.rb @@ -71,7 +71,7 @@ module SchemaStatements alias orig_create_table create_table def create_table(table_name, options = {}, &block) ret = orig_create_table(table_name, **options.except(:partition_key), &block) - if options[:partition_key] && options[:partition_key].to_s != 'id' + if options[:id] != false && options[:partition_key] && options[:partition_key].to_s != 'id' execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{table_name}_pkey" execute "ALTER TABLE #{table_name} ADD PRIMARY KEY(\"#{options[:partition_key]}\", id)" end From 881bbb1eec532f6f2fe3c4a5a66bbd61c01d7b56 Mon Sep 17 00:00:00 2001 From: Doug Edey Date: Wed, 30 Nov 2022 09:14:05 -0500 Subject: [PATCH 059/105] Handle updating a record tenant from nil to a reference --- CHANGELOG.md | 2 ++ lib/activerecord-multi-tenant/model_extensions.rb | 6 ++++-- .../activerecord-multi-tenant/model_extensions_spec.rb | 10 ++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 961a5086..91521a79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +* Handle changing tenant from `nil` to a value [#173](https://github.com/citusdata/activerecord-multi-tenant/pull/173) + ## 2.1.6 2022-11-23 * Fix undefined wrap_methods error & wrap_methods version check [#170](https://github.com/citusdata/activerecord-multi-tenant/pull/170) diff --git a/lib/activerecord-multi-tenant/model_extensions.rb b/lib/activerecord-multi-tenant/model_extensions.rb index 1cfa75b2..d9722b60 100644 --- a/lib/activerecord-multi-tenant/model_extensions.rb +++ b/lib/activerecord-multi-tenant/model_extensions.rb @@ -102,7 +102,8 @@ def inherited(subclass) include to_include around_save -> (record, block) { - if persisted? && MultiTenant.current_tenant_id.nil? + record_tenant = record.attribute_was(partition_key) + if persisted? && MultiTenant.current_tenant_id.nil? && !record_tenant.nil? MultiTenant.with(record.public_send(partition_key)) { block.call } else block.call @@ -110,7 +111,8 @@ def inherited(subclass) } around_update -> (record, block) { - if MultiTenant.current_tenant_id.nil? + record_tenant = record.attribute_was(partition_key) + if MultiTenant.current_tenant_id.nil? && !record_tenant.nil? MultiTenant.with(record.public_send(partition_key)) { block.call } else block.call diff --git a/spec/activerecord-multi-tenant/model_extensions_spec.rb b/spec/activerecord-multi-tenant/model_extensions_spec.rb index 41cc6a98..a81aee40 100644 --- a/spec/activerecord-multi-tenant/model_extensions_spec.rb +++ b/spec/activerecord-multi-tenant/model_extensions_spec.rb @@ -209,6 +209,16 @@ def self.name expect(record.account_id).to eq(nil) end + it 'handles changing tenant from nil to a value' do + record = OptionalSubTask.create(sub_task_id: sub_task.id) + expect(record.reload.sub_task).to eq(sub_task) + expect(record.account_id).to eq(nil) + + record.account = account + record.save! + expect(record.reload.account_id).to eq(account.id) + end + it 'handles has_many through' do MultiTenant.with(account) do expect(project.sub_tasks).to eq [sub_task] From a0f4370555274638b5cbf84e5e986dd1caa2ecb2 Mon Sep 17 00:00:00 2001 From: Shayon Mukherjee Date: Fri, 2 Dec 2022 11:21:43 -0500 Subject: [PATCH 060/105] Only attempt to reload with MultiTenant when parition_key is present When using gems like graphql or similar, they ca returns records from association which do not contain the `partition_key` column. In such cases, we'd see `ActiveModel::MissingAttributeError: missing attribute` error raised. This change makes it so that we just call the upstream/original method (`original_method_name`) when there is no `partition_key` present in the record. Thus, resulting in no exception. Also, adding a proof guard to ensure `partition_key` function is present on the class (`owner.class`). That is also another possibility of errors, especially when mocking models or overriding models in specs. Due to the way the ActiveRecord is overriden, we can also run into errors is "NoMethodError" for `partition_key` --- lib/activerecord-multi-tenant/multi_tenant.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/activerecord-multi-tenant/multi_tenant.rb b/lib/activerecord-multi-tenant/multi_tenant.rb index 4769035d..0bea7117 100644 --- a/lib/activerecord-multi-tenant/multi_tenant.rb +++ b/lib/activerecord-multi-tenant/multi_tenant.rb @@ -117,7 +117,7 @@ def self.wrap_methods(klass, owner, *method_names) klass.class_eval <<-CODE, __FILE__, __LINE__ + 1 alias_method :#{original_method_name}, :#{method_name} def #{method_name}(*args, &block) - if MultiTenant.multi_tenant_model_for_table(#{owner}.class.table_name).present? && #{owner}.persisted? && MultiTenant.current_tenant_id.nil? + 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) MultiTenant.with(#{owner}.public_send(#{owner}.class.partition_key)) { #{original_method_name}(*args, &block) } else #{original_method_name}(*args, &block) @@ -133,7 +133,7 @@ def self.wrap_methods(klass, owner, *method_names) klass.class_eval <<-CODE, __FILE__, __LINE__ + 1 alias_method :#{original_method_name}, :#{method_name} def #{method_name}(...) - if MultiTenant.multi_tenant_model_for_table(#{owner}.class.table_name).present? && #{owner}.persisted? && MultiTenant.current_tenant_id.nil? + 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) MultiTenant.with(#{owner}.public_send(#{owner}.class.partition_key)) { #{original_method_name}(...) } else #{original_method_name}(...) From ebb197544e5ee4a9853ba245c0047887b33c5020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Tue, 6 Dec 2022 15:55:56 +0000 Subject: [PATCH 061/105] 2.2.0: Remove support for Ruby 2.5 & ActiveRecord 5.2 --- .github/workflows/CI.yml | 16 ------------- Appraisals | 24 ------------------- CHANGELOG.md | 4 ++++ README.md | 2 +- activerecord-multi-tenant.gemspec | 2 +- gemfiles/active_record_5.2.gemfile | 16 ------------- gemfiles/rails_5.2.gemfile | 16 ------------- .../model_extensions.rb | 1 - lib/activerecord-multi-tenant/version.rb | 2 +- 9 files changed, 7 insertions(+), 76 deletions(-) delete mode 100644 gemfiles/active_record_5.2.gemfile delete mode 100644 gemfiles/rails_5.2.gemfile diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index d22b6b67..718620cb 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -13,40 +13,24 @@ jobs: fail-fast: false matrix: ruby: - - '2.5' - '2.6' - '2.7' - '3.0' - '3.1' gemfile: - - rails_5.2 - rails_6.0 - rails_6.1 - rails_7.0 - - active_record_5.2 - active_record_6.0 - active_record_6.1 - active_record_7.0 prepared_statements: [true, false] exclude: # activesupport-7.0.0 requires ruby version >= 2.7.0 - - ruby: '2.5' - gemfile: 'rails_7.0' - - ruby: '2.5' - gemfile: 'active_record_7.0' - ruby: '2.6' gemfile: 'rails_7.0' - ruby: '2.6' gemfile: 'active_record_7.0' - # ruby >3 and activesupport 5.2 are not compatible - - ruby: '3.0' - gemfile: 'rails_5.2' - - ruby: '3.0' - gemfile: 'active_record_5.2' - - ruby: '3.1' - gemfile: 'rails_5.2' - - ruby: '3.1' - gemfile: 'active_record_5.2' name: Ruby ${{ matrix.ruby }} / ${{ matrix.gemfile }} ${{ (matrix.prepared_statements && 'w/ prepared statements') || '' }} env: BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile diff --git a/Appraisals b/Appraisals index bb141136..abac38c4 100644 --- a/Appraisals +++ b/Appraisals @@ -1,15 +1,3 @@ -appraise 'rails-5.2' do - gem 'rails', '~> 5.2.0' - gem 'i18n', '~> 0.9.5' - gem 'nokogiri', '~> 1.7.1' - gem 'nio4r', '~> 2.3.1' - gem 'sprockets', '~> 3.7.1' - gem 'byebug', '~> 11.0' - gem 'rake', '12.0.0' - gem 'redis', '3.3.3' - gem 'pry-byebug', '3.9.0' -end - appraise 'rails-6.0' do gem 'rails', '~> 6.0.3' end @@ -22,18 +10,6 @@ appraise 'rails-7.0' do gem 'rails', '~> 7.0.0' end -appraise 'active-record-5.2' do - gem 'activerecord', '~> 5.2.0' - gem 'i18n', '~> 0.9.5' - gem 'nokogiri', '~> 1.7.1' - gem 'nio4r', '~> 2.3.1' - gem 'sprockets', '~> 3.7.1' - gem 'byebug', '~> 11.0' - gem 'rake', '12.0.0' - gem 'redis', '3.3.3' - gem 'pry-byebug', '3.9.0' -end - appraise 'active-record-6.0' do gem 'activerecord', '~> 6.0.3' end diff --git a/CHANGELOG.md b/CHANGELOG.md index 91521a79..d5f205c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog +## 2.2.0 2022-12-06 * Handle changing tenant from `nil` to a value [#173](https://github.com/citusdata/activerecord-multi-tenant/pull/173) +* Allow Partitioned tables to be created without a primary key [#172](https://github.com/citusdata/activerecord-multi-tenant/pull/172) +* Only attempt to reload with MultiTenant when parition_key is present [#175](https://github.com/citusdata/activerecord-multi-tenant/pull/175) +* Remove support for Ruby 2.5 & ActiveRecord 5.2 ## 2.1.6 2022-11-23 * Fix undefined wrap_methods error & wrap_methods version check [#170](https://github.com/citusdata/activerecord-multi-tenant/pull/170) diff --git a/README.md b/README.md index 822ecfe5..3d589cea 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ gem 'activerecord-multi-tenant' ## Supported Rails versions -All Ruby on Rails versions starting with 5.2 or newer (up to 7.0) are supported. +All Ruby on Rails versions starting with 6.0 or newer (up to 7.0) are supported. This gem only supports ActiveRecord (the Rails default ORM), and not alternative ORMs like Sequel. diff --git a/activerecord-multi-tenant.gemspec b/activerecord-multi-tenant.gemspec index ac46476b..28aada5e 100644 --- a/activerecord-multi-tenant.gemspec +++ b/activerecord-multi-tenant.gemspec @@ -15,7 +15,7 @@ Gem::Specification.new do |s| s.homepage = 'https://github.com/citusdata/activerecord-multi-tenant' s.license = 'MIT' - s.add_dependency 'rails', '>= 5.2' + s.add_dependency 'rails', '>= 6' s.add_development_dependency 'rspec', '>= 3.0' s.add_development_dependency 'rspec-rails' diff --git a/gemfiles/active_record_5.2.gemfile b/gemfiles/active_record_5.2.gemfile deleted file mode 100644 index 40886171..00000000 --- a/gemfiles/active_record_5.2.gemfile +++ /dev/null @@ -1,16 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "appraisal" -gem "activerecord", "~> 5.2.0" -gem "i18n", "~> 0.9.5" -gem "nokogiri", "~> 1.7.1" -gem "nio4r", "~> 2.3.1" -gem "sprockets", "~> 3.7.1" -gem "byebug", "~> 11.0" -gem "rake", "12.0.0" -gem "redis", "3.3.3" -gem "pry-byebug", "3.9.0" - -gemspec path: "../" diff --git a/gemfiles/rails_5.2.gemfile b/gemfiles/rails_5.2.gemfile deleted file mode 100644 index 3124d81a..00000000 --- a/gemfiles/rails_5.2.gemfile +++ /dev/null @@ -1,16 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "appraisal" -gem "rails", "~> 5.2.0" -gem "i18n", "~> 0.9.5" -gem "nokogiri", "~> 1.7.1" -gem "nio4r", "~> 2.3.1" -gem "sprockets", "~> 3.7.1" -gem "byebug", "~> 11.0" -gem "rake", "12.0.0" -gem "redis", "3.3.3" -gem "pry-byebug", "3.9.0" - -gemspec path: "../" diff --git a/lib/activerecord-multi-tenant/model_extensions.rb b/lib/activerecord-multi-tenant/model_extensions.rb index d9722b60..08e588ee 100644 --- a/lib/activerecord-multi-tenant/model_extensions.rb +++ b/lib/activerecord-multi-tenant/model_extensions.rb @@ -75,7 +75,6 @@ def inherited(subclass) # Rails 5 `attribute_will_change!` uses the attribute-method-call rather than `read_attribute` # and will raise ActiveModel::MissingAttributeError if that column was not selected. # This is rescued as NoMethodError and in MRI attribute_was is assigned an arbitrary Object - # This is still true after the Rails 5.2 refactor was = send("#{partition_key}_was") was_nil_or_skipped = was.nil? || was.class == Object diff --git a/lib/activerecord-multi-tenant/version.rb b/lib/activerecord-multi-tenant/version.rb index a0a5bce4..1a1c9d45 100644 --- a/lib/activerecord-multi-tenant/version.rb +++ b/lib/activerecord-multi-tenant/version.rb @@ -1,3 +1,3 @@ module MultiTenant - VERSION = '2.1.6' + VERSION = '2.2.0' end From baf3a4d0a0a181d3375e17af2a0c130fa7711558 Mon Sep 17 00:00:00 2001 From: Michail Pantelelis Date: Thu, 2 Feb 2023 16:24:05 +0200 Subject: [PATCH 062/105] Update model_extensions.rb @primary_key is a BasicObject instance when not set implicitly or explicitly on the latest Rails 7.1 so this check fails its purpose. --- lib/activerecord-multi-tenant/model_extensions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/activerecord-multi-tenant/model_extensions.rb b/lib/activerecord-multi-tenant/model_extensions.rb index 08e588ee..d4a3292b 100644 --- a/lib/activerecord-multi-tenant/model_extensions.rb +++ b/lib/activerecord-multi-tenant/model_extensions.rb @@ -30,7 +30,7 @@ def partition_key # Avoid primary_key errors when using composite primary keys (e.g. id, tenant_id) def primary_key - return @primary_key if @primary_key + return @primary_key if defined?(PRIMARY_KEY_NOT_SET) ? !PRIMARY_KEY_NOT_SET.equal?(@primary_key) : @primary_key primary_object_keys = Array.wrap(connection.schema_cache.primary_keys(table_name)) - [partition_key] From e3dcc7d0ffa846b7ae3ce21afc61cb66884ffede Mon Sep 17 00:00:00 2001 From: Gurkan Indibay Date: Fri, 24 Mar 2023 12:54:24 +0300 Subject: [PATCH 063/105] Updates citus versions and actions version --- .github/workflows/CI.yml | 2 +- docker-compose.yml | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 718620cb..1cbc05ea 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -36,7 +36,7 @@ jobs: BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile PREPARED_STATEMENTS: ${{ matrix.prepared_statements && '1' }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - run: | docker-compose up -d - uses: ruby/setup-ruby@v1 diff --git a/docker-compose.yml b/docker-compose.yml index 696e8064..11eae894 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,21 +2,22 @@ version: '2.1' services: master: - image: 'citusdata/citus:7.5.1' + container_name: "${COMPOSE_PROJECT_NAME:-citus}_master" + image: 'citusdata/citus:${CITUS_VERSION:-11.2}' ports: ['5600:5432'] labels: ['com.citusdata.role=Master'] volumes: ['/var/run/postgresql'] manager: container_name: "${COMPOSE_PROJECT_NAME:-citus}_manager" - image: 'citusdata/membership-manager:0.1.0' + image: 'citusdata/membership-manager:0.2.0' volumes: ['/var/run/docker.sock:/var/run/docker.sock'] depends_on: { master: { condition: service_healthy } } worker1: - image: 'citusdata/citus:7.5.1' + image: 'citusdata/citus:${CITUS_VERSION:-11.2}' labels: ['com.citusdata.role=Worker'] depends_on: { manager: { condition: service_healthy } } worker2: - image: 'citusdata/citus:7.5.1' + image: 'citusdata/citus:${CITUS_VERSION:-11.2}' labels: ['com.citusdata.role=Worker'] depends_on: { manager: { condition: service_healthy } } healthcheck: From dd35cf3ad5f7f87e6d48784bb05e9005a9370d04 Mon Sep 17 00:00:00 2001 From: Gurkan Indibay Date: Fri, 24 Mar 2023 13:09:24 +0300 Subject: [PATCH 064/105] Adds the django-multitenant docker-compose --- docker-compose.yml | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 11eae894..84260ca7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,24 +4,29 @@ services: master: container_name: "${COMPOSE_PROJECT_NAME:-citus}_master" image: 'citusdata/citus:${CITUS_VERSION:-11.2}' - ports: ['5600:5432'] - labels: ['com.citusdata.role=Master'] - volumes: ['/var/run/postgresql'] - manager: - container_name: "${COMPOSE_PROJECT_NAME:-citus}_manager" - image: 'citusdata/membership-manager:0.2.0' - volumes: ['/var/run/docker.sock:/var/run/docker.sock'] - depends_on: { master: { condition: service_healthy } } + ports: [ '5600:5432' ] + labels: [ 'com.citusdata.role=Master' ] + environment: + - POSTGRES_HOST_AUTH_METHOD=trust + command: -c fsync=off -c full_page_writes=off worker1: image: 'citusdata/citus:${CITUS_VERSION:-11.2}' - labels: ['com.citusdata.role=Worker'] + ports: [ '5601:5432' ] + labels: [ 'com.citusdata.role=Worker' ] depends_on: { manager: { condition: service_healthy } } + environment: + - POSTGRES_HOST_AUTH_METHOD=trust + command: -c fsync=off -c full_page_writes=off worker2: image: 'citusdata/citus:${CITUS_VERSION:-11.2}' - labels: ['com.citusdata.role=Worker'] + ports: [ '5602:5432' ] + labels: [ 'com.citusdata.role=Worker' ] depends_on: { manager: { condition: service_healthy } } - healthcheck: - image: busybox - depends_on: - worker1: { condition: service_healthy } - worker2: { condition: service_healthy } + environment: + - POSTGRES_HOST_AUTH_METHOD=trust + command: -c fsync=off -c full_page_writes=off + manager: + container_name: "${COMPOSE_PROJECT_NAME:-citus}_manager" + image: 'citusdata/membership-manager:0.2.0' + volumes: [ '/var/run/docker.sock:/var/run/docker.sock' ] + depends_on: { master: { condition: service_healthy } } From c7ed74a3a1783e9e913e78f3f25320631e794991 Mon Sep 17 00:00:00 2001 From: Gurkan Indibay Date: Fri, 24 Mar 2023 13:18:41 +0300 Subject: [PATCH 065/105] Adds citus version into matrix --- .../{CI.yml => active-record-multi-tenant-tests.yml} | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) rename .github/workflows/{CI.yml => active-record-multi-tenant-tests.yml} (89%) diff --git a/.github/workflows/CI.yml b/.github/workflows/active-record-multi-tenant-tests.yml similarity index 89% rename from .github/workflows/CI.yml rename to .github/workflows/active-record-multi-tenant-tests.yml index 1cbc05ea..e438b381 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/active-record-multi-tenant-tests.yml @@ -1,4 +1,4 @@ -name: CI +name: Active Record Multi-Tenant Tests on: push: @@ -24,6 +24,9 @@ jobs: - active_record_6.0 - active_record_6.1 - active_record_7.0 + citus_version: + - 10 + -11 prepared_statements: [true, false] exclude: # activesupport-7.0.0 requires ruby version >= 2.7.0 @@ -35,6 +38,7 @@ jobs: env: BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile PREPARED_STATEMENTS: ${{ matrix.prepared_statements && '1' }} + CITUS_VERSION: ${{ matrix.citus_version }} steps: - uses: actions/checkout@v3 - run: | From 47f39dbd089e323f65161362071ac2dda5ff496d Mon Sep 17 00:00:00 2001 From: Gurkan Indibay Date: Fri, 24 Mar 2023 13:21:33 +0300 Subject: [PATCH 066/105] Fixes Citus version list --- .github/workflows/active-record-multi-tenant-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/active-record-multi-tenant-tests.yml b/.github/workflows/active-record-multi-tenant-tests.yml index e438b381..bf4d2842 100644 --- a/.github/workflows/active-record-multi-tenant-tests.yml +++ b/.github/workflows/active-record-multi-tenant-tests.yml @@ -26,7 +26,7 @@ jobs: - active_record_7.0 citus_version: - 10 - -11 + - 11 prepared_statements: [true, false] exclude: # activesupport-7.0.0 requires ruby version >= 2.7.0 From 88c50ed8cdaee0dffc2b11ad72bc35b1d6c3b729 Mon Sep 17 00:00:00 2001 From: Gurkan Indibay Date: Fri, 24 Mar 2023 13:22:19 +0300 Subject: [PATCH 067/105] Convert citus version into string --- .github/workflows/active-record-multi-tenant-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/active-record-multi-tenant-tests.yml b/.github/workflows/active-record-multi-tenant-tests.yml index bf4d2842..14d73949 100644 --- a/.github/workflows/active-record-multi-tenant-tests.yml +++ b/.github/workflows/active-record-multi-tenant-tests.yml @@ -25,8 +25,8 @@ jobs: - active_record_6.1 - active_record_7.0 citus_version: - - 10 - - 11 + - '10' + - '11' prepared_statements: [true, false] exclude: # activesupport-7.0.0 requires ruby version >= 2.7.0 From db0c9b604f904da861765a43fef34b09b326bd8a Mon Sep 17 00:00:00 2001 From: Gurkan Indibay Date: Fri, 24 Mar 2023 13:30:36 +0300 Subject: [PATCH 068/105] Adds citus version into job name --- .github/workflows/active-record-multi-tenant-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/active-record-multi-tenant-tests.yml b/.github/workflows/active-record-multi-tenant-tests.yml index 14d73949..d557d1c8 100644 --- a/.github/workflows/active-record-multi-tenant-tests.yml +++ b/.github/workflows/active-record-multi-tenant-tests.yml @@ -34,7 +34,7 @@ jobs: gemfile: 'rails_7.0' - ruby: '2.6' gemfile: 'active_record_7.0' - name: Ruby ${{ matrix.ruby }} / ${{ matrix.gemfile }} ${{ (matrix.prepared_statements && 'w/ prepared statements') || '' }} + name: Ruby ${{ matrix.ruby }} / ${{ matrix.gemfile }} ${{ (matrix.prepared_statements && 'w/ prepared statements') || '' }} / Citus ${{ matrix.citus_version }} env: BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile PREPARED_STATEMENTS: ${{ matrix.prepared_statements && '1' }} From 331850f0069afa0236056f0d4250376ffb53b4ad Mon Sep 17 00:00:00 2001 From: Gurkan Indibay Date: Fri, 24 Mar 2023 15:33:49 +0300 Subject: [PATCH 069/105] Adds codecov integration --- .github/workflows/active-record-multi-tenant-tests.yml | 3 +++ Gemfile | 1 + spec/spec_helper.rb | 6 ++++++ 3 files changed, 10 insertions(+) diff --git a/.github/workflows/active-record-multi-tenant-tests.yml b/.github/workflows/active-record-multi-tenant-tests.yml index d557d1c8..e0fa0f1f 100644 --- a/.github/workflows/active-record-multi-tenant-tests.yml +++ b/.github/workflows/active-record-multi-tenant-tests.yml @@ -49,3 +49,6 @@ jobs: bundler-cache: true - run: | bundle exec rake spec + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 diff --git a/Gemfile b/Gemfile index e37a382a..ec814b39 100644 --- a/Gemfile +++ b/Gemfile @@ -2,4 +2,5 @@ source 'https://rubygems.org' gemspec +gem 'codecov', require: false, group: 'test' gem 'appraisal' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index bc4ef575..37a5ece5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -10,6 +10,12 @@ require 'bundler' Bundler.require(:default, :development) +require 'simplecov' +SimpleCov.start + +require 'codecov' +SimpleCov.formatter = SimpleCov::Formatter::Codecov + dbconfig = YAML::load(IO.read(File.join(File.dirname(__FILE__), 'database.yml'))) ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), "debug.log")) ActiveRecord::Base.establish_connection(dbconfig['test']) From 8dd50ccd37037c84aaa02fc0efaa4e2d737ee21e Mon Sep 17 00:00:00 2001 From: Gurkan Indibay Date: Fri, 24 Mar 2023 15:42:23 +0300 Subject: [PATCH 070/105] Adds codecov into activrecord dependency --- activerecord-multi-tenant.gemspec | 1 + 1 file changed, 1 insertion(+) diff --git a/activerecord-multi-tenant.gemspec b/activerecord-multi-tenant.gemspec index 28aada5e..5c6f428f 100644 --- a/activerecord-multi-tenant.gemspec +++ b/activerecord-multi-tenant.gemspec @@ -25,4 +25,5 @@ Gem::Specification.new do |s| s.add_development_dependency 'thor' s.add_development_dependency 'pry' s.add_development_dependency 'pry-byebug' + s.add_development_dependency 'codecov' end From 767bdb4a9c8987562fb2d40ccd12c608af6a8e0e Mon Sep 17 00:00:00 2001 From: Gurkan Indibay Date: Fri, 24 Mar 2023 15:53:57 +0300 Subject: [PATCH 071/105] Removes eol ruby versions --- .../workflows/active-record-multi-tenant-tests.yml | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/.github/workflows/active-record-multi-tenant-tests.yml b/.github/workflows/active-record-multi-tenant-tests.yml index e0fa0f1f..be2b93e0 100644 --- a/.github/workflows/active-record-multi-tenant-tests.yml +++ b/.github/workflows/active-record-multi-tenant-tests.yml @@ -13,10 +13,9 @@ jobs: fail-fast: false matrix: ruby: - - '2.6' - - '2.7' - '3.0' - '3.1' + - '3.2' gemfile: - rails_6.0 - rails_6.1 @@ -28,13 +27,8 @@ jobs: - '10' - '11' prepared_statements: [true, false] - exclude: - # activesupport-7.0.0 requires ruby version >= 2.7.0 - - ruby: '2.6' - gemfile: 'rails_7.0' - - ruby: '2.6' - gemfile: 'active_record_7.0' - name: Ruby ${{ matrix.ruby }} / ${{ matrix.gemfile }} ${{ (matrix.prepared_statements && 'w/ prepared statements') || '' }} / Citus ${{ matrix.citus_version }} + + name: Rb:${{ matrix.ruby }}/${{ matrix.gemfile }} ${{ (matrix.prepared_statements && 'w/ps') || '' }}/Cts ${{ matrix.citus_version }} env: BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile PREPARED_STATEMENTS: ${{ matrix.prepared_statements && '1' }} @@ -49,6 +43,6 @@ jobs: bundler-cache: true - run: | bundle exec rake spec - + - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 From 63542e9328eba4243075f4760feb0fddbff12cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCrkan=20=C4=B0ndibay?= Date: Tue, 28 Mar 2023 15:49:24 +0300 Subject: [PATCH 072/105] Removes prepared statement cases from tests (#181) Since Active Record 5.1 ActiveRecord::Base.connection.prepared_statements is true and testing for false is irrelevant since it makes the application prone to SQL injection --- .github/workflows/active-record-multi-tenant-tests.yml | 4 +--- spec/spec_helper.rb | 4 ---- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/active-record-multi-tenant-tests.yml b/.github/workflows/active-record-multi-tenant-tests.yml index be2b93e0..d7976ae8 100644 --- a/.github/workflows/active-record-multi-tenant-tests.yml +++ b/.github/workflows/active-record-multi-tenant-tests.yml @@ -26,12 +26,10 @@ jobs: citus_version: - '10' - '11' - prepared_statements: [true, false] - name: Rb:${{ matrix.ruby }}/${{ matrix.gemfile }} ${{ (matrix.prepared_statements && 'w/ps') || '' }}/Cts ${{ matrix.citus_version }} + name: Rb:${{ matrix.ruby }}/${{ matrix.gemfile }} / Cts ${{ matrix.citus_version }} env: BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile - PREPARED_STATEMENTS: ${{ matrix.prepared_statements && '1' }} CITUS_VERSION: ${{ matrix.citus_version }} steps: - uses: actions/checkout@v3 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 37a5ece5..5e5cab0d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -48,10 +48,6 @@ class Application < Rails::Application; end MultiTenantTest::Application.config.secret_token = 'x' * 40 MultiTenantTest::Application.config.secret_key_base = 'y' * 40 -def uses_prepared_statements? - ActiveRecord::Base.connection.prepared_statements -end - def with_belongs_to_required_by_default(&block) default_value = ActiveRecord::Base.belongs_to_required_by_default ActiveRecord::Base.belongs_to_required_by_default = true From 41b5c147117b97a762be9fcc0da783b89a656650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCrkan=20=C4=B0ndibay?= Date: Sun, 2 Apr 2023 18:33:58 +0300 Subject: [PATCH 073/105] Enables appraisal usage (#182) * Enables appraisal usage * Removes unnecessary gemfiles * Clarifies job names --- .../active-record-multi-tenant-tests.yml | 18 +++++++++--------- gemfiles/.bundle/config | 2 -- gemfiles/active_record_6.0.gemfile | 8 -------- gemfiles/active_record_6.1.gemfile | 8 -------- gemfiles/active_record_7.0.gemfile | 8 -------- gemfiles/rails_6.0.gemfile | 8 -------- gemfiles/rails_6.1.gemfile | 8 -------- gemfiles/rails_7.0.gemfile | 8 -------- 8 files changed, 9 insertions(+), 59 deletions(-) delete mode 100644 gemfiles/.bundle/config delete mode 100644 gemfiles/active_record_6.0.gemfile delete mode 100644 gemfiles/active_record_6.1.gemfile delete mode 100644 gemfiles/active_record_7.0.gemfile delete mode 100644 gemfiles/rails_6.0.gemfile delete mode 100644 gemfiles/rails_6.1.gemfile delete mode 100644 gemfiles/rails_7.0.gemfile diff --git a/.github/workflows/active-record-multi-tenant-tests.yml b/.github/workflows/active-record-multi-tenant-tests.yml index d7976ae8..4b8f7b6a 100644 --- a/.github/workflows/active-record-multi-tenant-tests.yml +++ b/.github/workflows/active-record-multi-tenant-tests.yml @@ -16,20 +16,20 @@ jobs: - '3.0' - '3.1' - '3.2' - gemfile: - - rails_6.0 - - rails_6.1 - - rails_7.0 - - active_record_6.0 - - active_record_6.1 - - active_record_7.0 + appraisal: + - rails-6.0 + - rails-6.1 + - rails-7.0 + - active-record-6.0 + - active-record-6.1 + - active-record-7.0 citus_version: - '10' - '11' - name: Rb:${{ matrix.ruby }}/${{ matrix.gemfile }} / Cts ${{ matrix.citus_version }} + name: Ruby ${{ matrix.ruby }}/${{ matrix.gemfile }} / Citus ${{ matrix.citus_version }} env: - BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile + APPRAISAL: ${{ matrix.appraisal }} CITUS_VERSION: ${{ matrix.citus_version }} steps: - uses: actions/checkout@v3 diff --git a/gemfiles/.bundle/config b/gemfiles/.bundle/config deleted file mode 100644 index c127f802..00000000 --- a/gemfiles/.bundle/config +++ /dev/null @@ -1,2 +0,0 @@ ---- -BUNDLE_RETRY: "1" diff --git a/gemfiles/active_record_6.0.gemfile b/gemfiles/active_record_6.0.gemfile deleted file mode 100644 index 34c84998..00000000 --- a/gemfiles/active_record_6.0.gemfile +++ /dev/null @@ -1,8 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "appraisal" -gem "activerecord", "~> 6.0.3" - -gemspec path: "../" diff --git a/gemfiles/active_record_6.1.gemfile b/gemfiles/active_record_6.1.gemfile deleted file mode 100644 index ea2599ce..00000000 --- a/gemfiles/active_record_6.1.gemfile +++ /dev/null @@ -1,8 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "appraisal" -gem "activerecord", "~> 6.1.0" - -gemspec path: "../" diff --git a/gemfiles/active_record_7.0.gemfile b/gemfiles/active_record_7.0.gemfile deleted file mode 100644 index 76f57c2e..00000000 --- a/gemfiles/active_record_7.0.gemfile +++ /dev/null @@ -1,8 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "appraisal" -gem "activerecord", "~> 7.0.0" - -gemspec path: "../" diff --git a/gemfiles/rails_6.0.gemfile b/gemfiles/rails_6.0.gemfile deleted file mode 100644 index def1fb38..00000000 --- a/gemfiles/rails_6.0.gemfile +++ /dev/null @@ -1,8 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "appraisal" -gem "rails", "~> 6.0.3" - -gemspec path: "../" diff --git a/gemfiles/rails_6.1.gemfile b/gemfiles/rails_6.1.gemfile deleted file mode 100644 index 9b9a89ff..00000000 --- a/gemfiles/rails_6.1.gemfile +++ /dev/null @@ -1,8 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "appraisal" -gem "rails", "~> 6.1.0" - -gemspec path: "../" diff --git a/gemfiles/rails_7.0.gemfile b/gemfiles/rails_7.0.gemfile deleted file mode 100644 index 75887596..00000000 --- a/gemfiles/rails_7.0.gemfile +++ /dev/null @@ -1,8 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "appraisal" -gem "rails", "~> 7.0.0" - -gemspec path: "../" From 325f4860f59d596e8889e476a022410575c23e17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCrkan=20=C4=B0ndibay?= Date: Sat, 15 Apr 2023 00:38:33 +0300 Subject: [PATCH 074/105] Fixes rubocop warnings (#184) --- .../active-record-multi-tenant-tests.yml | 16 +- .gitignore | 1 + .rubocop.yml | 46 +++ Gemfile | 4 +- Rakefile | 2 +- activerecord-multi-tenant.gemspec | 51 +-- .../arel_visitors_depth_first.rb | 357 ++++++++--------- .../controller_extensions.rb | 10 +- .../fast_truncate.rb | 5 +- lib/activerecord-multi-tenant/migrations.rb | 26 +- .../model_extensions.rb | 91 +++-- lib/activerecord-multi-tenant/multi_tenant.rb | 61 ++- .../query_monitor.rb | 20 +- .../query_rewriter.rb | 185 +++++---- lib/activerecord-multi-tenant/sidekiq.rb | 41 +- .../string_extension.rb | 16 + lib/activerecord-multi-tenant/version.rb | 2 +- lib/activerecord-multi-tenant/with_lock.rb | 12 +- ...tenant.rb => activerecord_multi_tenant.rb} | 7 +- .../controller_extensions_spec.rb | 5 +- .../fast_truncate_spec.rb | 14 +- .../model_extensions_spec.rb | 367 +++++++++++------- .../multi_tenant_spec.rb | 28 +- .../query_rewriter_spec.rb | 119 +++--- .../record_finding_spec.rb | 22 +- .../record_modifications_spec.rb | 8 +- .../schema_dumper_tester.rb | 0 .../activerecord-multi-tenant/sidekiq_spec.rb | 20 +- spec/schema.rb | 6 +- spec/spec_helper.rb | 15 +- 30 files changed, 921 insertions(+), 636 deletions(-) create mode 100644 .rubocop.yml create mode 100644 lib/activerecord-multi-tenant/string_extension.rb rename lib/{activerecord-multi-tenant.rb => activerecord_multi_tenant.rb} (71%) delete mode 100644 spec/activerecord-multi-tenant/schema_dumper_tester.rb diff --git a/.github/workflows/active-record-multi-tenant-tests.yml b/.github/workflows/active-record-multi-tenant-tests.yml index 4b8f7b6a..eca8d3b5 100644 --- a/.github/workflows/active-record-multi-tenant-tests.yml +++ b/.github/workflows/active-record-multi-tenant-tests.yml @@ -33,14 +33,22 @@ jobs: CITUS_VERSION: ${{ matrix.citus_version }} steps: - uses: actions/checkout@v3 - - run: | - docker-compose up -d + + - name: Start Citus Database environment + run: docker-compose up -d + - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - - run: | - bundle exec rake spec + + - name: Rubocop static code analysis + run: | + gem install rubocop + rubocop + + - name: Execute tests + run: bundle exec rake spec - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 diff --git a/.gitignore b/.gitignore index a14359c7..3c01aa0c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ pkg/ *.*~ Gemfile.lock *.gemfile.lock +.idea/ diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..8b4c4380 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,46 @@ +# This is an example RuboCop configuration file with some commonly used options. + +# Run RuboCop on all Ruby files, except those in `vendor` and `node_modules` directories +AllCops: + Exclude: + - 'vendor/**/*' + - 'node_modules/**/*' + +Style/FrozenStringLiteralComment: + Enabled: false + +Style/Documentation: + Exclude: + - '**/*.rb' + Enabled: false + +Lint/ConstantDefinitionInBlock: + Enabled: false + +Style/ClassAndModuleChildren: + Enabled: false + +Metrics/BlockLength: + Max: 650 + +Metrics/MethodLength: + Max: 150 + +Metrics/ClassLength: + Max: 200 + +Metrics/ModuleLength: + Max: 200 + +Metrics/AbcSize: + Enabled: false + +Metrics/PerceivedComplexity: + Enabled: false + +Metrics/CyclomaticComplexity: + Enabled: false + +Metrics/BlockNesting: + Enabled: false + diff --git a/Gemfile b/Gemfile index ec814b39..d0c6aacd 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' gemspec - -gem 'codecov', require: false, group: 'test' gem 'appraisal' +gem 'codecov', require: false, group: 'test' +gem 'rubocop', require: false, group: 'test' diff --git a/Rakefile b/Rakefile index 65c7261d..fb8d39a3 100644 --- a/Rakefile +++ b/Rakefile @@ -4,4 +4,4 @@ require 'bundler/gem_tasks' require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) -task :default => :spec +task default: :spec diff --git a/activerecord-multi-tenant.gemspec b/activerecord-multi-tenant.gemspec index 5c6f428f..a90fc884 100644 --- a/activerecord-multi-tenant.gemspec +++ b/activerecord-multi-tenant.gemspec @@ -1,29 +1,34 @@ -$:.push File.expand_path('../lib', __FILE__) +$LOAD_PATH.push File.expand_path('lib', __dir__) require 'activerecord-multi-tenant/version' -Gem::Specification.new do |s| - s.name = 'activerecord-multi-tenant' - s.version = MultiTenant::VERSION - s.summary = 'ActiveRecord/Rails integration for multi-tenant databases, in particular the Citus extension for PostgreSQL' - s.description = '' - s.authors = ['Citus Data'] - s.email = 'engage@citusdata.com' +Gem::Specification.new do |spec| + spec.name = 'activerecord-multi-tenant' + spec.version = MultiTenant::VERSION + spec.summary = 'ActiveRecord/Rails integration for multi-tenant databases, '\ + 'in particular the Citus extension for PostgreSQL' + spec.description = '' + spec.authors = ['Citus Data'] + spec.email = 'engage@citusdata.com' + spec.required_ruby_version = '>= 3.0.0' - s.files = `git ls-files`.split("\n") - s.test_files = `git ls-files -- {spec}/*`.split("\n") - s.require_paths = ['lib'] - s.homepage = 'https://github.com/citusdata/activerecord-multi-tenant' - s.license = 'MIT' + spec.files = `git ls-files`.split("\n") + spec.test_files = `git ls-files -- {spec}/*`.split("\n") + spec.require_paths = ['lib'] + spec.homepage = 'https://github.com/citusdata/activerecord-multi-tenant' + spec.license = 'MIT' - s.add_dependency 'rails', '>= 6' + spec.add_dependency 'rails', '>= 6' - s.add_development_dependency 'rspec', '>= 3.0' - s.add_development_dependency 'rspec-rails' - s.add_development_dependency 'pg' - s.add_development_dependency 'rake' - s.add_development_dependency 'sidekiq' - s.add_development_dependency 'thor' - s.add_development_dependency 'pry' - s.add_development_dependency 'pry-byebug' - s.add_development_dependency 'codecov' + spec.add_development_dependency 'anbt-sql-formatter' + spec.add_development_dependency 'codecov' + spec.add_development_dependency 'pg' + spec.add_development_dependency 'pry' + spec.add_development_dependency 'pry-byebug' + spec.add_development_dependency 'rake' + spec.add_development_dependency 'rspec', '>= 3.0' + spec.add_development_dependency 'rspec-rails' + spec.add_development_dependency 'rubocop' + spec.add_development_dependency 'sidekiq' + + spec.add_development_dependency 'thor' end diff --git a/lib/activerecord-multi-tenant/arel_visitors_depth_first.rb b/lib/activerecord-multi-tenant/arel_visitors_depth_first.rb index 5a3a18d9..a0c0f3b2 100644 --- a/lib/activerecord-multi-tenant/arel_visitors_depth_first.rb +++ b/lib/activerecord-multi-tenant/arel_visitors_depth_first.rb @@ -1,200 +1,209 @@ +# frozen_string_literal: true + module MultiTenant class ArelVisitorsDepthFirst < Arel::Visitors::Visitor def initialize(block = nil) - @block = block || Proc.new + @block = block || proc super() end private - def visit(o, _ = nil) - super - @block.call o - end + def visit(obj, _ = nil) + super + @block.call obj + end - def unary(o) - visit o.expr - end - alias :visit_Arel_Nodes_Else :unary - alias :visit_Arel_Nodes_Group :unary - alias :visit_Arel_Nodes_Cube :unary - alias :visit_Arel_Nodes_RollUp :unary - alias :visit_Arel_Nodes_GroupingSet :unary - alias :visit_Arel_Nodes_GroupingElement :unary - alias :visit_Arel_Nodes_Grouping :unary - alias :visit_Arel_Nodes_Having :unary - alias :visit_Arel_Nodes_Lateral :unary - alias :visit_Arel_Nodes_Limit :unary - alias :visit_Arel_Nodes_Not :unary - alias :visit_Arel_Nodes_Offset :unary - alias :visit_Arel_Nodes_On :unary - alias :visit_Arel_Nodes_Ordering :unary - alias :visit_Arel_Nodes_Ascending :unary - alias :visit_Arel_Nodes_Descending :unary - alias :visit_Arel_Nodes_UnqualifiedColumn :unary - alias :visit_Arel_Nodes_OptimizerHints :unary - alias :visit_Arel_Nodes_ValuesList :unary - - def function(o) - visit o.expressions - visit o.alias - visit o.distinct - end - alias :visit_Arel_Nodes_Avg :function - alias :visit_Arel_Nodes_Exists :function - alias :visit_Arel_Nodes_Max :function - alias :visit_Arel_Nodes_Min :function - alias :visit_Arel_Nodes_Sum :function - - def visit_Arel_Nodes_NamedFunction(o) - visit o.name - visit o.expressions - visit o.distinct - visit o.alias - end + def unary(obj) + visit obj.expr + end + alias visit_Arel_Nodes_Else unary + alias visit_Arel_Nodes_Group unary + alias visit_Arel_Nodes_Cube unary + alias visit_Arel_Nodes_RollUp unary + alias visit_Arel_Nodes_GroupingSet unary + alias visit_Arel_Nodes_GroupingElement unary + alias visit_Arel_Nodes_Grouping unary + alias visit_Arel_Nodes_Having unary + alias visit_Arel_Nodes_Lateral unary + alias visit_Arel_Nodes_Limit unary + alias visit_Arel_Nodes_Not unary + alias visit_Arel_Nodes_Offset unary + alias visit_Arel_Nodes_On unary + alias visit_Arel_Nodes_Ordering unary + alias visit_Arel_Nodes_Ascending unary + alias visit_Arel_Nodes_Descending unary + alias visit_Arel_Nodes_UnqualifiedColumn unary + alias visit_Arel_Nodes_OptimizerHints unary + alias visit_Arel_Nodes_ValuesList unary + + def function(obj) + visit obj.expressions + visit obj.alias + visit obj.distinct + end + alias visit_Arel_Nodes_Avg function + alias visit_Arel_Nodes_Exists function + alias visit_Arel_Nodes_Max function + alias visit_Arel_Nodes_Min function + alias visit_Arel_Nodes_Sum function + + # rubocop:disable Naming/MethodName + + def visit_Arel_Nodes_NamedFunction(obj) + visit obj.name + visit obj.expressions + visit obj.distinct + visit obj.alias + end - def visit_Arel_Nodes_Count(o) - visit o.expressions - visit o.alias - visit o.distinct - end + def visit_Arel_Nodes_Count(obj) + visit obj.expressions + visit obj.alias + visit obj.distinct + end - def visit_Arel_Nodes_Case(o) - visit o.case - visit o.conditions - visit o.default - end + def visit_Arel_Nodes_Case(obj) + visit obj.case + visit obj.conditions + visit obj.default + end - def nary(o) - o.children.each { |child| visit child } - end - alias :visit_Arel_Nodes_And :nary + def nary(obj) + obj.children.each { |child| visit child } + end + alias visit_Arel_Nodes_And nary - def binary(o) - visit o.left - visit o.right - end - alias :visit_Arel_Nodes_As :binary - alias :visit_Arel_Nodes_Assignment :binary - alias :visit_Arel_Nodes_Between :binary - alias :visit_Arel_Nodes_Concat :binary - alias :visit_Arel_Nodes_DeleteStatement :binary - alias :visit_Arel_Nodes_DoesNotMatch :binary - alias :visit_Arel_Nodes_Equality :binary - alias :visit_Arel_Nodes_FullOuterJoin :binary - alias :visit_Arel_Nodes_GreaterThan :binary - alias :visit_Arel_Nodes_GreaterThanOrEqual :binary - alias :visit_Arel_Nodes_In :binary - alias :visit_Arel_Nodes_InfixOperation :binary - alias :visit_Arel_Nodes_JoinSource :binary - alias :visit_Arel_Nodes_InnerJoin :binary - alias :visit_Arel_Nodes_LessThan :binary - alias :visit_Arel_Nodes_LessThanOrEqual :binary - alias :visit_Arel_Nodes_Matches :binary - alias :visit_Arel_Nodes_NotEqual :binary - alias :visit_Arel_Nodes_NotIn :binary - alias :visit_Arel_Nodes_NotRegexp :binary - alias :visit_Arel_Nodes_IsNotDistinctFrom :binary - alias :visit_Arel_Nodes_IsDistinctFrom :binary - alias :visit_Arel_Nodes_Or :binary - alias :visit_Arel_Nodes_OuterJoin :binary - alias :visit_Arel_Nodes_Regexp :binary - alias :visit_Arel_Nodes_RightOuterJoin :binary - alias :visit_Arel_Nodes_TableAlias :binary - alias :visit_Arel_Nodes_When :binary - - def visit_Arel_Nodes_StringJoin(o) - visit o.left - end + def binary(obj) + visit obj.left + visit obj.right + end + alias visit_Arel_Nodes_As binary + alias visit_Arel_Nodes_Assignment binary + alias visit_Arel_Nodes_Between binary + alias visit_Arel_Nodes_Concat binary + alias visit_Arel_Nodes_DeleteStatement binary + alias visit_Arel_Nodes_DoesNotMatch binary + alias visit_Arel_Nodes_Equality binary + alias visit_Arel_Nodes_FullOuterJoin binary + alias visit_Arel_Nodes_GreaterThan binary + alias visit_Arel_Nodes_GreaterThanOrEqual binary + alias visit_Arel_Nodes_In binary + alias visit_Arel_Nodes_InfixOperation binary + alias visit_Arel_Nodes_JoinSource binary + alias visit_Arel_Nodes_InnerJoin binary + alias visit_Arel_Nodes_LessThan binary + alias visit_Arel_Nodes_LessThanOrEqual binary + alias visit_Arel_Nodes_Matches binary + alias visit_Arel_Nodes_NotEqual binary + alias visit_Arel_Nodes_NotIn binary + alias visit_Arel_Nodes_NotRegexp binary + alias visit_Arel_Nodes_IsNotDistinctFrom binary + alias visit_Arel_Nodes_IsDistinctFrom binary + alias visit_Arel_Nodes_Or binary + alias visit_Arel_Nodes_OuterJoin binary + alias visit_Arel_Nodes_Regexp binary + alias visit_Arel_Nodes_RightOuterJoin binary + alias visit_Arel_Nodes_TableAlias binary + alias visit_Arel_Nodes_When binary + + def visit_Arel_Nodes_StringJoin(obj) + visit obj.left + end - def visit_Arel_Attribute(o) - visit o.relation - visit o.name - end - alias :visit_Arel_Attributes_Integer :visit_Arel_Attribute - alias :visit_Arel_Attributes_Float :visit_Arel_Attribute - alias :visit_Arel_Attributes_String :visit_Arel_Attribute - alias :visit_Arel_Attributes_Time :visit_Arel_Attribute - alias :visit_Arel_Attributes_Boolean :visit_Arel_Attribute - alias :visit_Arel_Attributes_Attribute :visit_Arel_Attribute - alias :visit_Arel_Attributes_Decimal :visit_Arel_Attribute - - def visit_Arel_Table(o) - visit o.name - end + def visit_Arel_Attribute(obj) + visit obj.relation + visit obj.name + end + alias visit_Arel_Attributes_Integer visit_Arel_Attribute + alias visit_Arel_Attributes_Float visit_Arel_Attribute + alias visit_Arel_Attributes_String visit_Arel_Attribute + alias visit_Arel_Attributes_Time visit_Arel_Attribute + alias visit_Arel_Attributes_Boolean visit_Arel_Attribute + alias visit_Arel_Attributes_Attribute visit_Arel_Attribute + alias visit_Arel_Attributes_Decimal visit_Arel_Attribute + + def visit_Arel_Table(obj) + visit obj.name + end - def terminal(o) - end - alias :visit_ActiveSupport_Multibyte_Chars :terminal - alias :visit_ActiveSupport_StringInquirer :terminal - alias :visit_Arel_Nodes_Lock :terminal - alias :visit_Arel_Nodes_Node :terminal - alias :visit_Arel_Nodes_SqlLiteral :terminal - alias :visit_Arel_Nodes_BindParam :terminal - alias :visit_Arel_Nodes_Window :terminal - alias :visit_Arel_Nodes_True :terminal - alias :visit_Arel_Nodes_False :terminal - alias :visit_BigDecimal :terminal - alias :visit_Class :terminal - alias :visit_Date :terminal - alias :visit_DateTime :terminal - alias :visit_FalseClass :terminal - alias :visit_Float :terminal - alias :visit_Integer :terminal - alias :visit_NilClass :terminal - alias :visit_String :terminal - alias :visit_Symbol :terminal - alias :visit_Time :terminal - alias :visit_TrueClass :terminal - - def visit_Arel_Nodes_InsertStatement(o) - visit o.relation - visit o.columns - visit o.values - end + def terminal(obj); end + alias visit_ActiveSupport_Multibyte_Chars terminal + alias visit_ActiveSupport_StringInquirer terminal + alias visit_Arel_Nodes_Lock terminal + alias visit_Arel_Nodes_Node terminal + alias visit_Arel_Nodes_SqlLiteral terminal + alias visit_Arel_Nodes_BindParam terminal + alias visit_Arel_Nodes_Window terminal + alias visit_Arel_Nodes_True terminal + alias visit_Arel_Nodes_False terminal + alias visit_BigDecimal terminal + alias visit_Class terminal + alias visit_Date terminal + alias visit_DateTime terminal + alias visit_FalseClass terminal + alias visit_Float terminal + alias visit_Integer terminal + alias visit_NilClass terminal + alias visit_String terminal + alias visit_Symbol terminal + alias visit_Time terminal + alias visit_TrueClass terminal + + def visit_Arel_Nodes_InsertStatement(obj) + visit obj.relation + visit obj.columns + visit obj.values + end - def visit_Arel_Nodes_SelectCore(o) - visit o.projections - visit o.source - visit o.wheres - visit o.groups - visit o.windows - visit o.havings - end + def visit_Arel_Nodes_SelectCore(obj) + visit obj.projections + visit obj.source + visit obj.wheres + visit obj.groups + visit obj.windows + visit obj.havings + end - def visit_Arel_Nodes_SelectStatement(o) - visit o.cores - visit o.orders - visit o.limit - visit o.lock - visit o.offset - end + def visit_Arel_Nodes_SelectStatement(obj) + visit obj.cores + visit obj.orders + visit obj.limit + visit obj.lock + visit obj.offset + end - def visit_Arel_Nodes_UpdateStatement(o) - visit o.relation - visit o.values - visit o.wheres - visit o.orders - visit o.limit - end + def visit_Arel_Nodes_UpdateStatement(obj) + visit obj.relation + visit obj.values + visit obj.wheres + visit obj.orders + visit obj.limit + end - def visit_Arel_Nodes_Comment(o) - visit o.values - end + def visit_Arel_Nodes_Comment(obj) + visit obj.values + end - def visit_Array(o) - o.each { |i| visit i } - end - alias :visit_Set :visit_Array + def visit_Array(obj) + obj.each { |i| visit i } + end + alias visit_Set visit_Array - def visit_Hash(o) - o.each { |k, v| visit(k); visit(v) } + def visit_Hash(obj) + obj.each do |k, v| + visit(k) + visit(v) end + end - DISPATCH = dispatch_cache + DISPATCH = dispatch_cache - def get_dispatch_cache - DISPATCH - end + # rubocop:disable Naming/AccessorMethodName + def get_dispatch_cache + DISPATCH + end + # rubocop:enable Naming/AccessorMethodName + # rubocop:enable Naming/MethodName end end diff --git a/lib/activerecord-multi-tenant/controller_extensions.rb b/lib/activerecord-multi-tenant/controller_extensions.rb index 4586f02f..b0d7ecf8 100644 --- a/lib/activerecord-multi-tenant/controller_extensions.rb +++ b/lib/activerecord-multi-tenant/controller_extensions.rb @@ -1,16 +1,18 @@ +# frozen_string_literal: true + module MultiTenant module ControllerExtensions def set_current_tenant_through_filter - self.class_eval do - if respond_to?(:helper_method) - helper_method :current_tenant - end + class_eval do + helper_method :current_tenant if respond_to?(:helper_method) private + # rubocop:disable Naming/AccessorMethodName def set_current_tenant(current_tenant_object) MultiTenant.current_tenant = current_tenant_object end + # rubocop:enable Naming/AccessorMethodName def current_tenant MultiTenant.current_tenant diff --git a/lib/activerecord-multi-tenant/fast_truncate.rb b/lib/activerecord-multi-tenant/fast_truncate.rb index 160b4e55..b7db4677 100644 --- a/lib/activerecord-multi-tenant/fast_truncate.rb +++ b/lib/activerecord-multi-tenant/fast_truncate.rb @@ -13,7 +13,8 @@ def self.run(exclude: ['schema_migrations']) needs_truncate boolean; BEGIN FOR t IN SELECT schemaname, tablename FROM pg_tables WHERE schemaname = 'public' AND tablename NOT IN (%s) LOOP - EXECUTE 'SELECT EXISTS (SELECT * from pg_class c WHERE c.relkind = ''S'' AND c.relname=''' || t.tablename || '_id_seq'')' into seq_exists; + EXECUTE 'SELECT EXISTS (SELECT * from pg_class c WHERE c.relkind = ''S'' + AND c.relname=''' || t.tablename || '_id_seq'')' into seq_exists; IF seq_exists THEN EXECUTE 'SELECT is_called FROM ' || t.tablename || '_id_seq' INTO needs_truncate; ELSE @@ -28,7 +29,7 @@ def self.run(exclude: ['schema_migrations']) IF array_length(tables, 1) > 0 THEN EXECUTE 'TRUNCATE TABLE ' || array_to_string(tables, ', ') || ' RESTART IDENTITY CASCADE'; END IF; - END$$;), exclude.map { |t| "'" + t + "'" }.join('\n')) + END$$;), exclude.map { |t| "'#{t}'" }.join('\n')) end end end diff --git a/lib/activerecord-multi-tenant/migrations.rb b/lib/activerecord-multi-tenant/migrations.rb index cda58112..237e5202 100644 --- a/lib/activerecord-multi-tenant/migrations.rb +++ b/lib/activerecord-multi-tenant/migrations.rb @@ -46,7 +46,8 @@ def execute_on_all_nodes(sql) execute "SELECT citus_run_on_all_workers($$#{sql}$$)" # initial citus_tools.sql with different names when nil # Do nothing, this is regular Postgres - else # 6.1 and newer + else + # 6.1 and newer execute "SELECT run_command_on_workers($$#{sql}$$)" end end @@ -69,6 +70,7 @@ module ActiveRecord module ConnectionAdapters # :nodoc: module SchemaStatements alias orig_create_table create_table + def create_table(table_name, options = {}, &block) ret = orig_create_table(table_name, **options.except(:partition_key), &block) if options[:id] != false && options[:partition_key] && options[:partition_key].to_s != 'id' @@ -86,18 +88,25 @@ class SchemaDumper private alias initialize_without_citus initialize + def initialize(connection, options = {}) initialize_without_citus(connection, options) - citus_version = begin - ActiveRecord::Migration.citus_version - rescue StandardError - # Handle the case where this gem is used with MySQL https://github.com/citusdata/activerecord-multi-tenant/issues/166 - nil - end + citus_version = + begin + ActiveRecord::Migration.citus_version + rescue StandardError + # Handle the case where this gem is used with MySQL https://github.com/citusdata/activerecord-multi-tenant/issues/166 + nil + end @distribution_columns = if citus_version.present? - @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| + query_to_execute = <<-SQL.strip + SELECT logicalrelid::regclass AS table_name, + column_to_column_name(logicalrelid, partkey) AS dist_col_name + FROM pg_dist_partition + SQL + @connection.execute(query_to_execute).to_h do |v| [v['table_name'], v['dist_col_name']] end else @@ -107,6 +116,7 @@ def initialize(connection, options = {}) # Support for create_distributed_table & create_reference_table alias table_without_citus table + def table(table, stream) table_without_citus(table, stream) table_name = remove_prefix_and_suffix(table) diff --git a/lib/activerecord-multi-tenant/model_extensions.rb b/lib/activerecord-multi-tenant/model_extensions.rb index d4a3292b..bef28561 100644 --- a/lib/activerecord-multi-tenant/model_extensions.rb +++ b/lib/activerecord-multi-tenant/model_extensions.rb @@ -8,13 +8,16 @@ def multi_tenant(tenant_name, options = {}) if to_s.underscore.to_sym == tenant_name || (!table_name.nil? && table_name.singularize.to_sym == tenant_name) unless MultiTenant.with_write_only_mode_enabled? # This is the tenant model itself. Workaround for https://github.com/citusdata/citus/issues/687 - before_create -> do - if self.class.columns_hash[self.class.primary_key].type == :uuid - self.id ||= SecureRandom.uuid - else - self.id ||= self.class.connection.select_value("SELECT nextval('#{self.class.table_name}_#{self.class.primary_key}_seq'::regclass)") - end - end + before_create lambda { + id = if self.class.columns_hash[self.class.primary_key].type == :uuid + SecureRandom.uuid + else + self.class.connection.select_value( + "SELECT nextval('#{self.class.table_name}_#{self.class.primary_key}_seq'::regclass)" + ) + end + self.id ||= id + } end else class << self @@ -24,24 +27,23 @@ def scoped_by_tenant? # Allow partition_key to be set from a superclass if not already set in this class def partition_key - @partition_key ||= ancestors.detect{ |k| k.instance_variable_get(:@partition_key) } - .try(:instance_variable_get, :@partition_key) + @partition_key ||= ancestors.detect { |k| k.instance_variable_get(:@partition_key) } + .try(:instance_variable_get, :@partition_key) end # Avoid primary_key errors when using composite primary keys (e.g. id, tenant_id) def primary_key - return @primary_key if defined?(PRIMARY_KEY_NOT_SET) ? !PRIMARY_KEY_NOT_SET.equal?(@primary_key) : @primary_key + if defined?(PRIMARY_KEY_NOT_SET) ? !PRIMARY_KEY_NOT_SET.equal?(@primary_key) : @primary_key + return @primary_key + end primary_object_keys = Array.wrap(connection.schema_cache.primary_keys(table_name)) - [partition_key] - if primary_object_keys.size == 1 - @primary_key = primary_object_keys.first - elsif connection.schema_cache.columns_hash(table_name).include? DEFAULT_ID_FIELD - @primary_key = DEFAULT_ID_FIELD - else - # table without a primary key and DEFAULT_ID_FIELD is not present in the table - @primary_key = nil - end + @primary_key = if primary_object_keys.size == 1 + primary_object_keys.first + elsif connection.schema_cache.columns_hash(table_name).include? DEFAULT_ID_FIELD + DEFAULT_ID_FIELD + end end def inherited(subclass) @@ -57,41 +59,49 @@ def inherited(subclass) # Create an implicit belongs_to association only if tenant class exists if MultiTenant.tenant_klass_defined?(tenant_name) - belongs_to tenant_name, **options.slice(:class_name, :inverse_of, :optional).merge(foreign_key: options[:partition_key]) + belongs_to tenant_name, **options.slice(:class_name, :inverse_of, :optional) + .merge(foreign_key: options[:partition_key]) end # New instances should have the tenant set - after_initialize Proc.new { |record| + after_initialize proc { |record| if MultiTenant.current_tenant_id && - (!record.attribute_present?(partition_key) || record.public_send(partition_key.to_sym).nil?) + (!record.attribute_present?(partition_key) || record.public_send(partition_key.to_sym).nil?) record.public_send("#{partition_key}=".to_sym, MultiTenant.current_tenant_id) end } to_include = Module.new do define_method "#{partition_key}=" do |tenant_id| - write_attribute("#{partition_key}", tenant_id) + write_attribute(partition_key.to_s, tenant_id) # Rails 5 `attribute_will_change!` uses the attribute-method-call rather than `read_attribute` # and will raise ActiveModel::MissingAttributeError if that column was not selected. # This is rescued as NoMethodError and in MRI attribute_was is assigned an arbitrary Object was = send("#{partition_key}_was") - was_nil_or_skipped = was.nil? || was.class == Object + was_nil_or_skipped = was.nil? || was.instance_of?(Object) + + if send("#{partition_key}_changed?") && persisted? && !was_nil_or_skipped + raise MultiTenant::TenantIsImmutable + end - raise MultiTenant::TenantIsImmutable if send("#{partition_key}_changed?") && persisted? && !was_nil_or_skipped tenant_id end if MultiTenant.tenant_klass_defined?(tenant_name) define_method "#{tenant_name}=" do |model| super(model) - raise MultiTenant::TenantIsImmutable if send("#{partition_key}_changed?") && persisted? && !send("#{partition_key}_was").nil? + if send("#{partition_key}_changed?") && persisted? && !send("#{partition_key}_was").nil? + raise MultiTenant::TenantIsImmutable + end + model end - define_method "#{tenant_name}" do - if !association(tenant_name.to_sym).loaded? && !MultiTenant.current_tenant_is_id? && MultiTenant.current_tenant_id && public_send(partition_key) == MultiTenant.current_tenant_id - return MultiTenant.current_tenant + define_method tenant_name.to_s do + if !association(tenant_name.to_sym).loaded? && !MultiTenant.current_tenant_is_id? && + MultiTenant.current_tenant_id && public_send(partition_key) == MultiTenant.current_tenant_id + MultiTenant.current_tenant else super() end @@ -100,7 +110,7 @@ def inherited(subclass) end include to_include - around_save -> (record, block) { + around_save lambda { |record, block| record_tenant = record.attribute_was(partition_key) if persisted? && MultiTenant.current_tenant_id.nil? && !record_tenant.nil? MultiTenant.with(record.public_send(partition_key)) { block.call } @@ -109,7 +119,7 @@ def inherited(subclass) end } - around_update -> (record, block) { + around_update lambda { |record, block| record_tenant = record.attribute_was(partition_key) if MultiTenant.current_tenant_id.nil? && !record_tenant.nil? MultiTenant.with(record.public_send(partition_key)) { block.call } @@ -118,7 +128,7 @@ def inherited(subclass) end } - around_destroy -> (record, block) { + around_destroy lambda { |record, block| if MultiTenant.current_tenant_id.nil? MultiTenant.with(record.public_send(partition_key)) { block.call } else @@ -133,23 +143,32 @@ def inherited(subclass) ActiveSupport.on_load(:active_record) do |base| base.extend MultiTenant::ModelExtensionsClassMethods - # Ensure we have current_tenant_id in where clause when a cached ActiveRecord instance is being reloaded, or update_columns without callbacks is called + # Ensure we have current_tenant_id in where clause when a cached ActiveRecord instance is being reloaded, + # or update_columns without callbacks is called MultiTenant.wrap_methods(ActiveRecord::Base, 'self', :delete, :reload, :update_columns) # Any queuries fired for fetching a singular association have the correct current_tenant_id in WHERE clause # reload is called anytime any record's association is accessed MultiTenant.wrap_methods(ActiveRecord::Associations::Association, 'owner', :reload) - # 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 - ActiveRecord::Associations::CollectionProxy.alias_method :equals_mt, :== # Hack to prevent syntax error due to invalid method name - ActiveRecord::Associations::CollectionProxy.alias_method :append_mt, :<< # Hack to prevent syntax error due to invalid method name - 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) + # 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 + ActiveRecord::Associations::CollectionProxy.alias_method \ + :equals_mt, :== # Hack to prevent syntax error due to invalid method name + ActiveRecord::Associations::CollectionProxy.alias_method \ + :append_mt, :<< # Hack to prevent syntax error due to invalid method name + 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) ActiveRecord::Associations::CollectionProxy.alias_method :==, :equals_mt ActiveRecord::Associations::CollectionProxy.alias_method :<<, :append_mt end class ActiveRecord::Associations::Association alias skip_statement_cache_orig skip_statement_cache? + def skip_statement_cache?(*scope) return true if klass.respond_to?(:scoped_by_tenant?) && klass.scoped_by_tenant? diff --git a/lib/activerecord-multi-tenant/multi_tenant.rb b/lib/activerecord-multi-tenant/multi_tenant.rb index 0bea7117..090217d5 100644 --- a/lib/activerecord-multi-tenant/multi_tenant.rb +++ b/lib/activerecord-multi-tenant/multi_tenant.rb @@ -10,22 +10,39 @@ def self.tenant_klass_defined?(tenant_name) end def self.partition_key(tenant_name) - "#{tenant_name.to_s}_id" + "#{tenant_name}_id" end + # rubocop:disable Style/ClassVars # In some cases we only have an ID - if defined we'll return the default tenant class in such cases - def self.default_tenant_class=(tenant_class); @@default_tenant_class = tenant_class; end - def self.default_tenant_class; @@default_tenant_class ||= nil; end + def self.default_tenant_class=(tenant_class) + @@default_tenant_class = tenant_class + end + + def self.default_tenant_class + @@default_tenant_class ||= nil + end # Write-only Mode - this only adds the tenant_id to new records, but doesn't # require its presence for SELECTs/UPDATEs/DELETEs - def self.enable_write_only_mode; @@enable_write_only_mode = true; end - def self.with_write_only_mode_enabled?; @@enable_write_only_mode ||= false; end + def self.enable_write_only_mode + @@enable_write_only_mode = true + end + + def self.with_write_only_mode_enabled? + @@enable_write_only_mode ||= false + end # Workaroud to make "with_lock" work until https://github.com/citusdata/citus/issues/1236 is fixed @@enable_with_lock_workaround = false - def self.enable_with_lock_workaround; @@enable_with_lock_workaround = true; end - def self.with_lock_workaround_enabled?; @@enable_with_lock_workaround; end + + def self.enable_with_lock_workaround + @@enable_with_lock_workaround = true + end + + def self.with_lock_workaround_enabled? + @@enable_with_lock_workaround + end # Registry that maps table names to models (used by the query rewriter) def self.register_multi_tenant_model(model_klass) @@ -38,17 +55,19 @@ def self.register_multi_tenant_model(model_klass) def self.multi_tenant_model_for_table(table_name) @@multi_tenant_models ||= [] - if !defined?(@@multi_tenant_model_table_names) - @@multi_tenant_model_table_names = @@multi_tenant_models.map { |model| + unless defined?(@@multi_tenant_model_table_names) + @@multi_tenant_model_table_names = @@multi_tenant_models.map do |model| [model.table_name, model] if model.table_name - }.compact.to_h + end.compact.to_h end @@multi_tenant_model_table_names[table_name.to_s] + # rubocop:enable Style/ClassVars end def self.multi_tenant_model_for_arel(arel) return nil unless arel.respond_to?(:ast) + if arel.ast.relation.is_a? Arel::Nodes::JoinSource MultiTenant.multi_tenant_model_for_table(arel.ast.relation.left.table_name) else @@ -74,7 +93,7 @@ def self.current_tenant_is_id? def self.current_tenant_class if current_tenant_is_id? - MultiTenant.default_tenant_class || fail('Only have tenant id, and no default tenant class set') + MultiTenant.default_tenant_class || raise('Only have tenant id, and no default tenant class set') elsif current_tenant MultiTenant.current_tenant.class.name end @@ -83,33 +102,37 @@ def self.current_tenant_class def self.load_current_tenant! return MultiTenant.current_tenant if MultiTenant.current_tenant && !current_tenant_is_id? raise 'MultiTenant.current_tenant must be set to load' if MultiTenant.current_tenant.nil? - klass = MultiTenant.default_tenant_class || fail('Only have tenant id, and no default tenant class set') + + klass = MultiTenant.default_tenant_class || raise('Only have tenant id, and no default tenant class set') self.current_tenant = klass.find(MultiTenant.current_tenant_id) end def self.with(tenant, &block) - return block.call if self.current_tenant == tenant - old_tenant = self.current_tenant + return block.call if current_tenant == tenant + + old_tenant = current_tenant begin self.current_tenant = tenant - return block.call + block.call ensure self.current_tenant = old_tenant end end def self.without(&block) - return block.call if self.current_tenant.nil? - old_tenant = self.current_tenant + return block.call if current_tenant.nil? + + old_tenant = current_tenant begin self.current_tenant = nil - return block.call + block.call ensure self.current_tenant = old_tenant end end - # 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 + # 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 if Gem::Version.create(RUBY_VERSION) < Gem::Version.new('3.0.0') def self.wrap_methods(klass, owner, *method_names) method_names.each do |method_name| diff --git a/lib/activerecord-multi-tenant/query_monitor.rb b/lib/activerecord-multi-tenant/query_monitor.rb index 097f9874..627a37aa 100644 --- a/lib/activerecord-multi-tenant/query_monitor.rb +++ b/lib/activerecord-multi-tenant/query_monitor.rb @@ -1,15 +1,27 @@ # Add generic warning when queries fail and there is no tenant set module MultiTenant + # rubocop:disable Style/ClassVars # Option to enable query monitor @@enable_query_monitor = false - def self.enable_query_monitor; @@enable_query_monitor = true; end - def self.query_monitor_enabled?; @@enable_query_monitor; end + + def self.enable_query_monitor + @@enable_query_monitor = true + end + + def self.query_monitor_enabled? + @@enable_query_monitor + end + + # rubocop:enable Style/ClassVars class QueryMonitor - def start(name, id, payload); end - def finish(name, id, payload) + def start(_name, _id, _payload) end + + def finish(_name, _id, payload) return unless MultiTenant.query_monitor_enabled? + return unless payload[:exception].present? && MultiTenant.current_tenant_id.nil? + Rails.logger.info 'WARNING: Tenant not present - make sure to add MultiTenant.with(tenant) { ... }' end end diff --git a/lib/activerecord-multi-tenant/query_rewriter.rb b/lib/activerecord-multi-tenant/query_rewriter.rb index ab9e039d..adef0dac 100644 --- a/lib/activerecord-multi-tenant/query_rewriter.rb +++ b/lib/activerecord-multi-tenant/query_rewriter.rb @@ -1,5 +1,5 @@ require 'active_record' -require_relative "./arel_visitors_depth_first.rb" unless Arel::Visitors.const_defined?(:DepthFirst) +require_relative './arel_visitors_depth_first' unless Arel::Visitors.const_defined?(:DepthFirst) module MultiTenant class Table @@ -9,9 +9,9 @@ def initialize(arel_table) @arel_table = arel_table end - def eql?(rhs) - self.class == rhs.class && - equality_fields.eql?(rhs.equality_fields) + def eql?(other) + self.class == other.class && + equality_fields.eql?(other.equality_fields) end def hash @@ -44,6 +44,7 @@ def discover_relations def visited_relation(relation) return unless @discovering + @known_relations << Table.new(relation) end @@ -56,9 +57,13 @@ def unhandled_relations end end - class ArelTenantVisitor < Arel::Visitors.const_defined?(:DepthFirst) ? Arel::Visitors::DepthFirst : ::MultiTenant::ArelVisitorsDepthFirst + class ArelTenantVisitor < if Arel::Visitors.const_defined?(:DepthFirst) + Arel::Visitors::DepthFirst + else + ::MultiTenant::ArelVisitorsDepthFirst + end def initialize(arel) - super(Proc.new {}) + super(proc {}) @statement_node_id = nil @contexts = [] @@ -68,61 +73,72 @@ def initialize(arel) attr_reader :contexts + # rubocop:disable Naming/MethodName def visit_Arel_Attributes_Attribute(*args) return if @current_context.nil? + super(*args) end - def visit_Arel_Nodes_Equality(o, *args) - if o.left.is_a?(Arel::Attributes::Attribute) - table_name = o.left.relation.table_name + def visit_Arel_Nodes_Equality(obj, *args) + if obj.left.is_a?(Arel::Attributes::Attribute) + table_name = obj.left.relation.table_name model = MultiTenant.multi_tenant_model_for_table(table_name) - @current_context.visited_handled_relation(o.left.relation) if model.present? && o.left.name.to_s == model.partition_key.to_s + if model.present? && obj.left.name.to_s == model.partition_key.to_s + @current_context.visited_handled_relation(obj.left.relation) + end end - super(o, *args) + super(obj, *args) end - def visit_MultiTenant_TenantEnforcementClause(o, *) - @current_context.visited_handled_relation(o.tenant_attribute.relation) + def visit_MultiTenant_TenantEnforcementClause(obj, *) + @current_context.visited_handled_relation(obj.tenant_attribute.relation) end - def visit_MultiTenant_TenantJoinEnforcementClause(o, *) - @current_context.visited_handled_relation(o.tenant_attribute.relation) + def visit_MultiTenant_TenantJoinEnforcementClause(obj, *) + @current_context.visited_handled_relation(obj.tenant_attribute.relation) end - def visit_Arel_Table(o, _collector = nil) - @current_context.visited_relation(o) if tenant_relation?(o.table_name) + def visit_Arel_Table(obj, _collector = nil) + @current_context.visited_relation(obj) if tenant_relation?(obj.table_name) end - alias :visit_Arel_Nodes_TableAlias :visit_Arel_Table - def visit_Arel_Nodes_SelectCore(o, *args) - nest_context(o) do + alias visit_Arel_Nodes_TableAlias visit_Arel_Table + + def visit_Arel_Nodes_SelectCore(obj, *_args) + nest_context(obj) do @current_context.discover_relations do - visit o.source + visit obj.source end - visit o.wheres - visit o.groups - visit o.windows - if defined?(o.having) - visit o.having + visit obj.wheres + visit obj.groups + visit obj.windows + if defined?(obj.having) + visit obj.having else - visit o.havings + visit obj.havings end end end - def visit_Arel_Nodes_OuterJoin(o, collector = nil) - nest_context(o) do + # rubocop:enable Naming/MethodName + + # rubocop:disable Naming/MethodName + def visit_Arel_Nodes_OuterJoin(obj, _collector = nil) + nest_context(obj) do @current_context.discover_relations do - visit o.left - visit o.right + visit obj.left + visit obj.right end end end - alias :visit_Arel_Nodes_FullOuterJoin :visit_Arel_Nodes_OuterJoin - alias :visit_Arel_Nodes_RightOuterJoin :visit_Arel_Nodes_OuterJoin - alias :visit_ActiveModel_Attribute :terminal + # rubocop:enable Naming/MethodName + + alias visit_Arel_Nodes_FullOuterJoin visit_Arel_Nodes_OuterJoin + alias visit_Arel_Nodes_RightOuterJoin visit_Arel_Nodes_OuterJoin + + alias visit_ActiveModel_Attribute terminal private @@ -138,13 +154,16 @@ def dispatch DISPATCH end + # rubocop:disable Naming/AccessorMethodName def get_dispatch_cache dispatch end - def nest_context(o) + # rubocop:enable Naming/AccessorMethodName + + def nest_context(obj) old_context = @current_context - @current_context = Context.new(o) + @current_context = Context.new(obj) @contexts << @current_context yield @@ -155,25 +174,31 @@ def nest_context(o) class BaseTenantEnforcementClause < Arel::Nodes::Node attr_reader :tenant_attribute + def initialize(tenant_attribute) + super() @tenant_attribute = tenant_attribute @tenant_model = MultiTenant.multi_tenant_model_for_table(tenant_attribute.relation.table_name) end - def to_s; to_sql; end - def to_str; to_sql; end + def to_s + to_sql + end + + def to_str + to_sql + end def to_sql(*) collector = Arel::Collectors::SQLString.new collector = @tenant_model.connection.visitor.accept tenant_arel, collector collector.value end - - end class TenantEnforcementClause < BaseTenantEnforcementClause private + def tenant_arel if defined?(Arel::Nodes::Quoted) @tenant_attribute.eq(Arel::Nodes::Quoted.new(MultiTenant.current_tenant_id)) @@ -183,9 +208,9 @@ def tenant_arel end end - class TenantJoinEnforcementClause < BaseTenantEnforcementClause attr_reader :table_left + def initialize(tenant_attribute, table_left) super(tenant_attribute) @table_left = table_left @@ -193,20 +218,23 @@ def initialize(tenant_attribute, table_left) end private + def tenant_arel @tenant_attribute.eq(@table_left[@model_left.partition_key]) end end - module TenantValueVisitor - def visit_MultiTenant_TenantEnforcementClause(o, collector) - collector << o + # rubocop:disable Naming/MethodName + def visit_MultiTenant_TenantEnforcementClause(obj, collector) + collector << obj end - def visit_MultiTenant_TenantJoinEnforcementClause(o, collector) - collector << o + def visit_MultiTenant_TenantJoinEnforcementClause(obj, collector) + collector << obj end + + # rubocop:enable Naming/MethodName end module DatabaseStatements @@ -254,11 +282,12 @@ def delete(arel, name = nil, binds = []) require 'active_record/relation' module ActiveRecord module QueryMethods - alias :build_arel_orig :build_arel + alias build_arel_orig build_arel + def build_arel(*args) arel = build_arel_orig(*args) - if !MultiTenant.with_write_only_mode_enabled? + unless MultiTenant.with_write_only_mode_enabled? visitor = MultiTenant::ArelTenantVisitor.new(arel) visitor.contexts.each do |context| @@ -270,45 +299,44 @@ def build_arel(*args) if MultiTenant.current_tenant_id enforcement_clause = MultiTenant::TenantEnforcementClause.new(relation.arel_table[model.partition_key]) case node - when Arel::Nodes::Join #Arel::Nodes::OuterJoin, Arel::Nodes::RightOuterJoin, Arel::Nodes::FullOuterJoin + when Arel::Nodes::Join # Arel::Nodes::OuterJoin, Arel::Nodes::RightOuterJoin, Arel::Nodes::FullOuterJoin node.right.expr = node.right.expr.and(enforcement_clause) when Arel::Nodes::SelectCore if node.wheres.empty? node.wheres = [enforcement_clause] + elsif node.wheres[0].is_a?(Arel::Nodes::And) + node.wheres[0].children << enforcement_clause else - if node.wheres[0].is_a?(Arel::Nodes::And) - node.wheres[0].children << enforcement_clause - else - node.wheres[0] = enforcement_clause.and(node.wheres[0]) - end + node.wheres[0] = enforcement_clause.and(node.wheres[0]) end else - raise "UnknownContext" + raise 'UnknownContext' end end - if node.is_a?(Arel::Nodes::SelectCore) || node.is_a?(Arel::Nodes::Join) - if node.is_a?Arel::Nodes::Join - node_list = [node] - else - node_list = node.source.right - end + next unless node.is_a?(Arel::Nodes::SelectCore) || node.is_a?(Arel::Nodes::Join) - node_list.select{ |n| n.is_a? Arel::Nodes::Join }.each do |node_join| - if !node_join.right - next - end - relation_right, relation_left = relations_from_node_join(node_join) + node_list = if node.is_a? Arel::Nodes::Join + [node] + else + node.source.right + end - next unless relation_right && relation_left + node_list.select { |n| n.is_a? Arel::Nodes::Join }.each do |node_join| + next unless node_join.right - model_right = MultiTenant.multi_tenant_model_for_table(relation_left.table_name) - model_left = MultiTenant.multi_tenant_model_for_table(relation_right.table_name) - if model_right && model_left - join_enforcement_clause = MultiTenant::TenantJoinEnforcementClause.new(relation_right[model_right.partition_key], relation_left) - node_join.right.expr = node_join.right.expr.and(join_enforcement_clause) - end - end + relation_right, relation_left = relations_from_node_join(node_join) + + next unless relation_right && relation_left + + model_right = MultiTenant.multi_tenant_model_for_table(relation_left.table_name) + model_left = MultiTenant.multi_tenant_model_for_table(relation_right.table_name) + next unless model_right && model_left + + join_enforcement_clause = MultiTenant::TenantJoinEnforcementClause.new( + relation_right[model_right.partition_key], relation_left + ) + node_join.right.expr = node_join.right.expr.and(join_enforcement_clause) end end end @@ -318,6 +346,7 @@ def build_arel(*args) end private + def relations_from_node_join(node_join) if node_join.right.expr.is_a?(Arel::Nodes::Equality) return node_join.right.expr.right.relation, node_join.right.expr.left.relation @@ -325,17 +354,17 @@ def relations_from_node_join(node_join) children = [node_join.right.expr.children].flatten - tenant_applied = children.any?{|c| c.is_a?(MultiTenant::TenantEnforcementClause) || c.is_a?(MultiTenant::TenantJoinEnforcementClause)} - if tenant_applied || children.empty? - return nil, nil + tenant_applied = children.any? do |c| + c.is_a?(MultiTenant::TenantEnforcementClause) || c.is_a?(MultiTenant::TenantJoinEnforcementClause) end + return nil, nil if tenant_applied || children.empty? child = children.first.respond_to?(:children) ? children.first.children.first : children.first if child.right.respond_to?(:relation) && child.left.respond_to?(:relation) return child.right.relation, child.left.relation end - return nil, nil + [nil, nil] end end end diff --git a/lib/activerecord-multi-tenant/sidekiq.rb b/lib/activerecord-multi-tenant/sidekiq.rb index 40cd9484..9f6b63fd 100644 --- a/lib/activerecord-multi-tenant/sidekiq.rb +++ b/lib/activerecord-multi-tenant/sidekiq.rb @@ -3,12 +3,14 @@ module Sidekiq::Middleware::MultiTenant # Get the current tenant and store in the message to be sent to Sidekiq. class Client - def call(worker_class, msg, queue, redis_pool) - msg['multi_tenant'] ||= - { - 'class' => MultiTenant.current_tenant_class, - 'id' => MultiTenant.current_tenant_id - } if MultiTenant.current_tenant.present? + def call(_worker_class, msg, _queue, _redis_pool) + if MultiTenant.current_tenant.present? + msg['multi_tenant'] ||= + { + 'class' => MultiTenant.current_tenant_class, + 'id' => MultiTenant.current_tenant_id + } + end yield end @@ -16,16 +18,14 @@ def call(worker_class, msg, queue, redis_pool) # Pull the tenant out and run the current thread with it. class Server - def call(worker_class, msg, queue) - if msg.has_key?('multi_tenant') + def call(_worker_class, msg, _queue, &block) + if msg.key?('multi_tenant') tenant = begin - msg['multi_tenant']['class'].constantize.find(msg['multi_tenant']['id']) - rescue ActiveRecord::RecordNotFound - msg['multi_tenant']['id'] - end - MultiTenant.with(tenant) do - yield + msg['multi_tenant']['class'].constantize.find(msg['multi_tenant']['id']) + rescue ActiveRecord::RecordNotFound + msg['multi_tenant']['id'] end + MultiTenant.with(tenant, &block) else yield end @@ -48,24 +48,25 @@ def call(worker_class, msg, queue) end end - module Sidekiq class Client def push_bulk_with_tenants(items) - job = items['jobs'].first - return [] unless job # no jobs to push - raise ArgumentError, "Bulk arguments must be an Array of Hashes: [{ 'args' => [1], 'tenant_id' => 1 }, ...]" if !job.is_a?(Hash) + first_job = items['jobs'].first + return [] unless first_job # no jobs to push + unless first_job.is_a?(Hash) + raise ArgumentError, "Bulk arguments must be an Array of Hashes: [{ 'args' => [1], 'tenant_id' => 1 }, ...]" + end normed = normalize_item(items.except('jobs').merge('args' => [])) payloads = items['jobs'].map do |job| MultiTenant.with(job['tenant_id']) do copy = normed.merge('args' => job['args'], 'jid' => SecureRandom.hex(12), 'enqueued_at' => Time.now.to_f) result = process_single(items['class'], copy) - result ? result : nil + result || nil end end.compact - raw_push(payloads) if !payloads.empty? + raw_push(payloads) unless payloads.empty? payloads.collect { |payload| payload['jid'] } end diff --git a/lib/activerecord-multi-tenant/string_extension.rb b/lib/activerecord-multi-tenant/string_extension.rb new file mode 100644 index 00000000..b9c04b9d --- /dev/null +++ b/lib/activerecord-multi-tenant/string_extension.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'anbt-sql-formatter/formatter' + +class String + def pretty_format_sql + rule = AnbtSql::Rule.new + rule.keyword = AnbtSql::Rule::KEYWORD_UPPER_CASE + %w[count sum substr date].each do |func_name| + rule.function_names << func_name.upcase + end + rule.indent_string = ' ' + formatter = AnbtSql::Formatter.new(rule) + formatter.format(dup) + end +end diff --git a/lib/activerecord-multi-tenant/version.rb b/lib/activerecord-multi-tenant/version.rb index 1a1c9d45..480a0194 100644 --- a/lib/activerecord-multi-tenant/version.rb +++ b/lib/activerecord-multi-tenant/version.rb @@ -1,3 +1,3 @@ module MultiTenant - VERSION = '2.2.0' + VERSION = '2.2.0'.freeze end diff --git a/lib/activerecord-multi-tenant/with_lock.rb b/lib/activerecord-multi-tenant/with_lock.rb index 14521017..82e4a96f 100644 --- a/lib/activerecord-multi-tenant/with_lock.rb +++ b/lib/activerecord-multi-tenant/with_lock.rb @@ -1,10 +1,16 @@ +# frozen_string_literal: true + # Workaround for https://github.com/citusdata/citus/issues/1236 # "SELECT ... FOR UPDATE is not supported for router-plannable queries" class ActiveRecord::Base - alias :lock_orig :lock! + alias lock_orig lock! + + # rubocop:disable Style/OptionalBooleanParameter + # Having errors when using lock: true def lock!(lock = true) - if lock && persisted? && self.class.respond_to?(:scoped_by_tenant?) && MultiTenant.current_tenant_id && MultiTenant.with_lock_workaround_enabled? + if lock && persisted? && self.class.respond_to?(:scoped_by_tenant?) && \ + MultiTenant.current_tenant_id && MultiTenant.with_lock_workaround_enabled? self.class.unscoped.where(id: id).update_all(id: id) # No-op UPDATE that locks the row reload # This is just to act similar to the default ActiveRecord approach, in case someone relies on the reload self @@ -12,4 +18,6 @@ def lock!(lock = true) lock_orig(lock) end end + + # rubocop:enable Style/OptionalBooleanParameter end diff --git a/lib/activerecord-multi-tenant.rb b/lib/activerecord_multi_tenant.rb similarity index 71% rename from lib/activerecord-multi-tenant.rb rename to lib/activerecord_multi_tenant.rb index 760126ed..284461d5 100644 --- a/lib/activerecord-multi-tenant.rb +++ b/lib/activerecord_multi_tenant.rb @@ -1,6 +1,6 @@ -if Object.const_defined?(:ActionController) - require_relative 'activerecord-multi-tenant/controller_extensions' -end +# frozen_string_literal: true + +require_relative 'activerecord-multi-tenant/controller_extensions' if Object.const_defined?(:ActionController) require_relative 'activerecord-multi-tenant/copy_from_client' require_relative 'activerecord-multi-tenant/fast_truncate' require_relative 'activerecord-multi-tenant/migrations' @@ -10,3 +10,4 @@ require_relative 'activerecord-multi-tenant/query_monitor' require_relative 'activerecord-multi-tenant/version' require_relative 'activerecord-multi-tenant/with_lock' +require_relative 'activerecord-multi-tenant/string_extension' diff --git a/spec/activerecord-multi-tenant/controller_extensions_spec.rb b/spec/activerecord-multi-tenant/controller_extensions_spec.rb index bceaa88c..6e663586 100644 --- a/spec/activerecord-multi-tenant/controller_extensions_spec.rb +++ b/spec/activerecord-multi-tenant/controller_extensions_spec.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + require 'spec_helper' -describe "Controller Extensions", type: :controller do +describe 'Controller Extensions', type: :controller do class Account attr_accessor :name end @@ -31,7 +33,6 @@ def index end end - class APIApplicationController < ActionController::API include Rails.application.routes.url_helpers set_current_tenant_through_filter diff --git a/spec/activerecord-multi-tenant/fast_truncate_spec.rb b/spec/activerecord-multi-tenant/fast_truncate_spec.rb index 634b958e..f665241b 100644 --- a/spec/activerecord-multi-tenant/fast_truncate_spec.rb +++ b/spec/activerecord-multi-tenant/fast_truncate_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe MultiTenant::FastTruncate do @@ -5,19 +7,19 @@ MultiTenant::FastTruncate.run end - it "truncates tables that have exactly one row inserted" do + it 'truncates tables that have exactly one row inserted' do Account.create! name: 'foo' - expect { + expect do MultiTenant::FastTruncate.run - }.to change { Account.count }.from(1).to(0) + end.to change { Account.count }.from(1).to(0) end - it "truncates tables that have more than one row inserted" do + it 'truncates tables that have more than one row inserted' do Account.create! name: 'foo' Account.create! name: 'bar' - expect { + expect do MultiTenant::FastTruncate.run - }.to change { Account.count }.from(2).to(0) + end.to change { Account.count }.from(2).to(0) end end diff --git a/spec/activerecord-multi-tenant/model_extensions_spec.rb b/spec/activerecord-multi-tenant/model_extensions_spec.rb index a81aee40..cc442064 100644 --- a/spec/activerecord-multi-tenant/model_extensions_spec.rb +++ b/spec/activerecord-multi-tenant/model_extensions_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe MultiTenant do @@ -10,11 +12,11 @@ end describe 'is_scoped_as_tenant should return the correct value when true' do - it {expect(Project.respond_to?(:scoped_by_tenant?)).to eq(true)} + it { expect(Project.respond_to?(:scoped_by_tenant?)).to eq(true) } end describe 'is_scoped_as_tenant should return the correct value when false' do - it {expect(UnscopedModel.respond_to?(:scoped_by_tenant?)).to eq(false)} + it { expect(UnscopedModel.respond_to?(:scoped_by_tenant?)).to eq(false) } end context 'immutability' do @@ -43,17 +45,17 @@ @account = Account.create! name: 'foo' MultiTenant.current_tenant = @account end - it {expect(Project.new.account_id).to eq(@account.id)} + it { expect(Project.new.account_id).to eq(@account.id) } it 'should handle partial selects' do project = Project.create! - expect{project = Project.select(:name).find(project.id)}.not_to raise_error + expect { project = Project.select(:name).find(project.id) }.not_to raise_error expect(project.account_id).to eq(@account.id) end end describe 'Handles custom partition_key on tenant model' do before do - @account = Account.create! name: 'foo' + @account = Account.create! name: 'foo' MultiTenant.current_tenant = @account @custom_partition_key_task = CustomPartitionKeyTask.create! name: 'foo' end @@ -70,11 +72,11 @@ it { expect(@partition_key_not_model_task.non_model_id).to be 77 } end - describe 'Tenant model with a nonstandard class name' do let(:account_klass) do Class.new(ActiveRecord::Base) do self.table_name = 'account' + def self.name 'UserAccount' end @@ -82,7 +84,7 @@ def self.name multi_tenant(:account) end end - it "does not register the tenant model" do + it 'does not register the tenant model' do expect(MultiTenant).not_to receive(:register_multi_tenant_model) account_klass end @@ -171,16 +173,16 @@ def self.name end end - describe "It should be possible to use aliased associations" do + describe 'It should be possible to use aliased associations' do before do @account = Account.create! name: 'baz' MultiTenant.current_tenant = @account end - it { expect(AliasedTask.create(:name => 'foo', :project_alias => @project2).valid?).to eq(true) } + it { expect(AliasedTask.create(name: 'foo', project_alias: @project2).valid?).to eq(true) } end - describe "It should be possible to use associations with partition_key from polymorphic" do + describe 'It should be possible to use associations with partition_key from polymorphic' do before do @account = Account.create!(name: 'foo') MultiTenant.current_tenant = @account @@ -237,7 +239,7 @@ def self.name MultiTenant.with(account) do sub_task manager - expect(Project.eager_load([{manager: :project}, {tasks: :project}]).first).to eq project + expect(Project.eager_load([{ manager: :project }, { tasks: :project }]).first).to eq project end end end @@ -248,7 +250,8 @@ def self.name it 'rewrites sub-selects correctly' do MultiTenant.with(account) do - expect(Project.where(id: Project.where(id: project.id)).where(id: Project.where(id: project.id)).first).to eq project + expect(Project.where(id: Project.where(id: project.id)) + .where(id: Project.where(id: project.id)).first).to eq project end end end @@ -277,34 +280,34 @@ def self.name end describe 'non-STI Subclass of abstract Multi Tenant Model' do - let(:tenant_id_1) { 42 } - let(:tenant_id_2) { 314158 } + let(:tenant_id1) { 42 } + let(:tenant_id2) { 314_158 } let(:name) { 'fooname' } - let(:subclass_task_1) do - MultiTenant.with(tenant_id_1) { SubclassTask.create! name: name } + let(:subclass_task1) do + MultiTenant.with(tenant_id1) { SubclassTask.create! name: name } end - let(:subclass_task_2) do - MultiTenant.with(tenant_id_2) { SubclassTask.create! name: name } + let(:subclass_task2) do + MultiTenant.with(tenant_id2) { SubclassTask.create! name: name } end before do - subclass_task_1 - subclass_task_2 + subclass_task1 + subclass_task2 end it 'injects tenant_id on create' do - expect(subclass_task_1.non_model_id).to be tenant_id_1 - expect(subclass_task_2.non_model_id).to be tenant_id_2 + expect(subclass_task1.non_model_id).to be tenant_id1 + expect(subclass_task2.non_model_id).to be tenant_id2 end it 'rewrites query' do - MultiTenant.with(tenant_id_1) do + MultiTenant.with(tenant_id1) do expect(SubclassTask.where(name: name).count).to eq 1 - expect(SubclassTask.where(name: name).first).to eq subclass_task_1 + expect(SubclassTask.where(name: name).first).to eq subclass_task1 end - MultiTenant.with(tenant_id_2) do + MultiTenant.with(tenant_id2) do expect(SubclassTask.where(name: name).count).to eq 1 - expect(SubclassTask.where(name: name).first).to eq subclass_task_2 + expect(SubclassTask.where(name: name).first).to eq subclass_task2 end end end @@ -316,20 +319,27 @@ def self.name it 'should add tenant condition to the queries when tenant is set' do expected_join_sql = <<-SQL.strip - 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 + 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 SQL MultiTenant.with(account) do - expect(Comment.joins(:task).to_sql).to eq(expected_join_sql) + expect(Comment.joins(:task).to_sql.pretty_format_sql).to eq(expected_join_sql.pretty_format_sql) end end it 'should add tenant condition to the queries when tenant is not set' do MultiTenant.without do expected_join_sql = <<-SQL.strip - 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" + 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" SQL - expect(Comment.joins(:task).to_sql).to eq(expected_join_sql) + expect(Comment.joins(:task).to_sql.pretty_format_sql).to eq(expected_join_sql.pretty_format_sql) end end end @@ -339,57 +349,63 @@ def self.name it 'should add tenant condition to the queries when tenant is set' do expected_join_sql = <<-SQL.strip - SELECT "projects".* FROM "projects" INNER JOIN "tasks" ON "tasks"."project_id" = "projects"."id" AND "tasks"."account_id" = 1 WHERE "projects"."account_id" = 1 + SELECT "projects".*#{' '} + FROM "projects"#{' '} + INNER JOIN "tasks" ON "tasks"."project_id" = "projects"."id"#{' '} + AND "tasks"."account_id" = 1#{' '} + WHERE "projects"."account_id" = 1 SQL MultiTenant.with(account) do - expect(Project.joins(:tasks).to_sql).to eq(expected_join_sql) + expect(Project.joins(:tasks).to_sql.pretty_format_sql).to eq(expected_join_sql.pretty_format_sql) end end it 'should add tenant condition to the queries when tenant is not set' do MultiTenant.without do expected_join_sql = <<-SQL.strip - SELECT "projects".* FROM "projects" INNER JOIN "tasks" ON "tasks"."project_id" = "projects"."id" AND "projects"."account_id" = "tasks"."account_id" + SELECT "projects".* + FROM "projects" + INNER JOIN "tasks" ON "tasks"."project_id" = "projects"."id" + AND "projects"."account_id" = "tasks"."account_id" SQL - expect(Project.joins(:tasks).to_sql).to eq(expected_join_sql) + expect(Project.joins(:tasks).to_sql.pretty_format_sql).to eq(expected_join_sql.pretty_format_sql) end end end end # ::with - describe "::with" do - it "should set current_tenant to the specified tenant inside the block" do - @account = Account.create!(:name => 'baz') + describe '::with' do + it 'should set current_tenant to the specified tenant inside the block' do + @account = Account.create!(name: 'baz') MultiTenant.with(@account) do expect(MultiTenant.current_tenant).to eq(@account) end end - it "should reset current_tenant to the previous tenant once exiting the block" do - @account1 = Account.create!(:name => 'foo') - @account2 = Account.create!(:name => 'bar') + it 'should reset current_tenant to the previous tenant once exiting the block' do + @account1 = Account.create!(name: 'foo') + @account2 = Account.create!(name: 'bar') MultiTenant.current_tenant = @account1 MultiTenant.with @account2 do - end expect(MultiTenant.current_tenant).to eq(@account1) end - it "should return the value of the block" do - @account1 = Account.create!(:name => 'foo') - @account2 = Account.create!(:name => 'bar') + it 'should return the value of the block' do + @account1 = Account.create!(name: 'foo') + @account2 = Account.create!(name: 'bar') MultiTenant.current_tenant = @account1 value = MultiTenant.with @account2 do - "something" + 'something' end - expect(value).to eq "something" + expect(value).to eq 'something' end it 'supports reload inside the block' do @@ -404,9 +420,9 @@ def self.name end # ::without - describe "::without" do - it "should unset current_tenant inside the block" do - @account = Account.create!(:name => 'baz') + describe '::without' do + it 'should unset current_tenant inside the block' do + @account = Account.create!(name: 'baz') MultiTenant.current_tenant = @account MultiTenant.without do @@ -414,26 +430,25 @@ def self.name end end - it "should reset current_tenant to the previous tenant once exiting the block" do - @account1 = Account.create!(:name => 'foo') + it 'should reset current_tenant to the previous tenant once exiting the block' do + @account1 = Account.create!(name: 'foo') MultiTenant.current_tenant = @account1 MultiTenant.without do - end expect(MultiTenant.current_tenant).to eq(@account1) end - it "should return the value of the block" do - @account1 = Account.create!(:name => 'foo') + it 'should return the value of the block' do + @account1 = Account.create!(name: 'foo') MultiTenant.current_tenant = @account1 value = MultiTenant.without do - "something" + 'something' end - expect(value).to eq "something" + expect(value).to eq 'something' end end @@ -471,13 +486,20 @@ def self.name end end - it "applies the team_id conditions in the where clause" do - option1 = <<-sql.strip - 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 - sql - option2 = <<-sql.strip - 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 - sql + it 'applies the team_id conditions in the where clause' do + option1 = <<-SQL.strip + 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 + SQL + option2 = <<-SQL.strip + 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 + SQL account1 = Account.create! name: 'Account 1' @@ -485,27 +507,38 @@ def self.name project1 = Project.create! name: 'Project 1' task1 = Task.create! name: 'Task 1', project: project1 subtask1 = SubTask.create! task: task1 - expect(project1.sub_tasks.to_sql).to eq(option1).or(eq(option2)) + expect(project1.sub_tasks.to_sql.pretty_format_sql) + .to eq(option1.pretty_format_sql).or(eq(option2.pretty_format_sql)) expect(project1.sub_tasks).to include(subtask1) end MultiTenant.without do - expected_sql = <<-sql - 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 - sql + expected_sql = <<-SQL + 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 + SQL project = Project.first - expect(project.sub_tasks.to_sql).to eq(expected_sql.strip) + expect(project.sub_tasks.to_sql.pretty_format_sql).to eq(expected_sql.strip.pretty_format_sql) end end - it "tests joins between distributed and reference table" do - option1 = <<-sql.strip - SELECT "categories".* FROM "categories" INNER JOIN "project_categories" ON "categories"."id" = "project_categories"."category_id" WHERE "project_categories"."project_id" = 1 AND "project_categories"."account_id" = 1 - sql - option2 = <<-sql.strip - SELECT "categories".* FROM "categories" INNER JOIN "project_categories" ON "categories"."id" = "project_categories"."category_id" WHERE "project_categories"."account_id" = 1 AND "project_categories"."project_id" = 1 - sql + it 'tests joins between distributed and reference table' do + option1 = <<-SQL.strip + SELECT "categories".*#{' '} + FROM "categories"#{' '} + INNER JOIN "project_categories" ON "categories"."id" = "project_categories"."category_id"#{' '} + WHERE "project_categories"."project_id" = 1 AND "project_categories"."account_id" = 1 + SQL + option2 = <<-SQL.strip + SELECT "categories".*#{' '} + FROM "categories"#{' '} + INNER JOIN "project_categories" ON "categories"."id" = "project_categories"."category_id"#{' '} + WHERE "project_categories"."account_id" = 1 AND "project_categories"."project_id" = 1 + SQL account1 = Account.create! name: 'Account 1' category1 = Category.create! name: 'Category 1' @@ -514,47 +547,73 @@ def self.name project1 = Project.create! name: 'Project 1' projectcategory = ProjectCategory.create! name: 'project cat 1', project: project1, category: category1 - expect(project1.categories.to_sql).to eq(option1).or(eq(option2)) + expect(project1.categories.to_sql.pretty_format_sql) + .to eq(option1.pretty_format_sql).or(eq(option2.pretty_format_sql)) expect(project1.categories).to include(category1) expect(project1.project_categories).to include(projectcategory) end MultiTenant.without do - expected_sql = <<-sql - SELECT "categories".* FROM "categories" INNER JOIN "project_categories" ON "categories"."id" = "project_categories"."category_id" WHERE "project_categories"."project_id" = 1 - sql + expected_sql = <<-SQL + SELECT "categories".*#{' '} + FROM "categories"#{' '} + INNER JOIN "project_categories" ON "categories"."id" = "project_categories"."category_id"#{' '} + WHERE "project_categories"."project_id" = 1 + SQL project = Project.first - expect(project.categories.to_sql).to eq(expected_sql.strip) + expect(project.categories.to_sql.pretty_format_sql) + .to eq(expected_sql.strip.pretty_format_sql) expect(project.categories).to include(category1) - expected_sql = <<-sql - 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 - sql - - expect(Project.where(account_id: 1).joins(:categories).to_sql).to eq(expected_sql.strip) + expected_sql = <<-SQL + 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 + SQL + + expect(Project.where(account_id: 1).joins(:categories) + .to_sql.pretty_format_sql) + .to eq(expected_sql.strip.pretty_format_sql) project = Project.where(account_id: 1).joins(:categories).first expect(project.categories).to include(category1) end end - - it "test eager_load" do + it 'test eager_load' do account1 = Account.create! name: 'Account 1' category1 = Category.create! name: 'Category 1' - option1 = <<-sql.strip - SELECT "projects"."id" AS t0_r0, "projects"."account_id" AS t0_r1, "projects"."name" AS t0_r2, "categories"."id" AS t1_r0, "categories"."name" AS t1_r1 FROM "projects" LEFT OUTER JOIN "project_categories" ON "project_categories"."project_id" = "projects"."id" AND "project_categories"."account_id" = 1 AND "projects"."account_id" = 1 LEFT OUTER JOIN "categories" ON "categories"."id" = "project_categories"."category_id" AND "project_categories"."account_id" = 1 WHERE "projects"."account_id" = 1 - sql - option2 = <<-sql.strip - 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"."account_id" = 1 AND "project_categories"."project_id" = "projects"."id" AND "projects"."account_id" = 1 LEFT OUTER JOIN "categories" ON "categories"."id" = "project_categories"."category_id" AND "project_categories"."account_id" = 1 WHERE "projects"."account_id" = 1 - sql + option1 = <<-SQL.strip + SELECT "projects"."id" AS t0_r0, "projects"."account_id" AS t0_r1, "projects"."name" AS t0_r2, + "categories"."id" AS t1_r0, "categories"."name" AS t1_r1 + FROM "projects" + LEFT OUTER JOIN "project_categories" + ON "project_categories"."project_id" = "projects"."id" AND "project_categories"."account_id" = 1 + AND "projects"."account_id" = 1#{' '} + LEFT OUTER JOIN "categories" ON "categories"."id" = "project_categories"."category_id" + AND "project_categories"."account_id" = 1 + WHERE "projects"."account_id" = 1 + SQL + option2 = <<-SQL.strip + 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"."account_id" = 1#{' '} + AND "project_categories"."project_id" = "projects"."id" AND "projects"."account_id" = 1#{' '} + LEFT OUTER JOIN "categories" ON "categories"."id" = "project_categories"."category_id"#{' '} + AND "project_categories"."account_id" = 1 WHERE "projects"."account_id" = 1 + SQL MultiTenant.with(account1) do project1 = Project.create! name: 'Project 1' projectcategory = ProjectCategory.create! name: 'project cat 1', project: project1, category: category1 - expect(Project.eager_load(:categories).to_sql).to eq(option1).or(eq(option2)) + expect(Project.eager_load(:categories).to_sql.pretty_format_sql) + .to eq(option1.pretty_format_sql).or(eq(option2.pretty_format_sql)) project = Project.eager_load(:categories).first expect(project.categories).to include(category1) @@ -562,92 +621,118 @@ def self.name end MultiTenant.without do - expected_sql = <<-sql - 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 - sql - - expect(Project.where(account_id: 1).eager_load(:categories).to_sql).to eq(expected_sql.strip) + expected_sql = <<-SQL + 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 + SQL + + expect(Project.where(account_id: 1).eager_load(:categories).to_sql.pretty_format_sql) + .to eq(expected_sql.strip.pretty_format_sql) project = Project.where(account_id: 1).eager_load(:categories).first expect(project.categories).to include(category1) - end end - it "test raw SQL joins" do + it 'test raw SQL joins' do account1 = Account.create! name: 'Account 1' category1 = Category.create! name: 'Category 1' MultiTenant.with(account1) do - option1 = <<-sql.strip - SELECT "tasks".* FROM "tasks" INNER JOIN "projects" ON "projects"."id" = "tasks"."project_id" AND "projects"."account_id" = 1 LEFT JOIN project_categories pc ON project.category_id = pc.id WHERE "tasks"."account_id" = 1 - sql - option2 = <<-sql.strip - SELECT "tasks".* FROM "tasks" INNER JOIN "projects" ON "projects"."account_id" = 1 AND "projects"."id" = "tasks"."project_id" LEFT JOIN project_categories pc ON project.category_id = pc.id WHERE "tasks"."account_id" = 1 - sql + option1 = <<-SQL.strip + SELECT "tasks".* FROM "tasks" + INNER JOIN "projects" ON "projects"."id" = "tasks"."project_id" AND "projects"."account_id" = 1 + LEFT JOIN project_categories pc ON project.category_id = pc.id#{' '} + WHERE "tasks"."account_id" = 1 + SQL + option2 = <<-SQL.strip + SELECT "tasks".* FROM "tasks" + INNER JOIN "projects" ON "projects"."account_id" = 1#{' '} + AND "projects"."id" = "tasks"."project_id" + LEFT JOIN project_categories pc ON project.category_id = pc.id#{' '} + WHERE "tasks"."account_id" = 1 + SQL project1 = Project.create! name: 'Project 1' - projectcategory = ProjectCategory.create! name: 'project cat 1', project: project1, category: category1 + ProjectCategory.create! name: 'project cat 1', project: project1, category: category1 project1.tasks.create! name: 'baz' - expect(Task.joins(:project).joins('LEFT JOIN project_categories pc ON project.category_id = pc.id').to_sql).to eq(option1).or(eq(option2)) + expect(Task.joins(:project) + .joins('LEFT JOIN project_categories pc ON project.category_id = pc.id').to_sql.pretty_format_sql) + .to eq(option1.pretty_format_sql).or(eq(option2.pretty_format_sql)) end MultiTenant.without do - expected_sql = <<-sql - 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 - sql - - 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) - + expected_sql = <<-SQL.strip + 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 + SQL + + expect(Task.where(account_id: 1).joins(:project) + .joins('LEFT JOIN project_categories pc ON project.category_id = pc.id') + .to_sql.pretty_format_sql).to eq(expected_sql.strip.pretty_format_sql) end - end - it "only applies clauses when a tenant is set" do + it 'only applies clauses when a tenant is set' do account = Account.create! name: 'Account 1' project = Project.create! name: 'Project 1', account: account project2 = Project.create! name: 'Project 2', account: Account.create!(name: 'Account2') MultiTenant.with(account) do - option1 = <<-sql.strip - SELECT "projects".* FROM "projects" WHERE "projects"."account_id" = #{account.id} AND "projects"."id" = $1 LIMIT $2 - sql - option2 = <<-sql.strip - SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 AND "projects"."account_id" = #{account.id} LIMIT $2 - sql - option3 = <<-sql.strip - SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 AND "projects"."account_id" = #{account.id} LIMIT $2 - sql + option1 = <<-SQL.strip + SELECT "projects".* FROM "projects"#{' '} + WHERE "projects"."account_id" = #{account.id} AND "projects"."id" = $1 LIMIT $2 + SQL + option2 = <<-SQL.strip + SELECT "projects".* FROM "projects"#{' '} + WHERE "projects"."id" = $1 AND "projects"."account_id" = #{account.id} LIMIT $2 + SQL + option3 = <<-SQL.strip + SELECT "projects".* FROM "projects" + WHERE "projects"."id" = $1 + AND "projects"."account_id" = #{account.id} LIMIT $2 + SQL # Couldn't make the following line pass for some reason, so came up with an uglier alternative - # expect(Project).to receive(:find_by_sql).with(eq(option1).or(eq(option2)).or(eq(option3)), any_args).and_call_original + # expect(Project).to receive(:find_by_sql).with(eq(option1). + # or(eq(option2)).or(eq(option3)), any_args).and_call_original expect(Project).to receive(:find_by_sql).and_wrap_original do |m, *args| - expect(args[0]).to(eq(option1).or(eq(option2)).or(eq(option3))) - m.call(args[0], args[1], preparable:args[2][:preparable]) + expect(args[0].pretty_format_sql).to(eq(option1.pretty_format_sql) + .or(eq(option2.pretty_format_sql)).or(eq(option3.pretty_format_sql))) + m.call(args[0], args[1], preparable: args[2][:preparable]) end expect(Project.find(project.id)).to eq(project) end MultiTenant.without do - option1 = <<-sql.strip - SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2 - sql - option2 = <<-sql.strip - SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2 - sql + option1 = <<-SQL.strip + SELECT "projects".* FROM "projects"#{' '} + WHERE "projects"."id" = $1 LIMIT $2 + SQL + option2 = <<-SQL.strip + SELECT "projects".* FROM "projects"#{' '} + WHERE "projects"."id" = $1 LIMIT $2 + SQL # Couldn't make the following line pass for some reason, so came up with an uglier alternative # expect(Project).to receive(:find_by_sql).with(eq(option1).or(eq(option2)), any_args).and_call_original expect(Project).to receive(:find_by_sql).and_wrap_original do |m, *args| - expect(args[0]).to(eq(option1).or(eq(option2))) - m.call(args[0], args[1], preparable:args[2][:preparable]) + expect(args[0].pretty_format_sql).to(eq(option1.pretty_format_sql).or(eq(option2.pretty_format_sql))) + m.call(args[0], args[1], preparable: args[2][:preparable]) end expect(Project.find(project2.id)).to eq(project2) end end - describe 'with unsaved association' do before do @account = Account.create!(name: 'reflection tenant') @@ -661,13 +746,13 @@ def self.name end end - it "test value of RETURNING insert in table with no pkey" do + it 'test value of RETURNING insert in table with no pkey' do account1 = Account.create(name: 'test1') MultiTenant.with(account1) do - allowed_place = AllowedPlace.create! name: 'something1' + AllowedPlace.create! name: 'something1' - project = Project.create! name: 'Project 1' + Project.create! name: 'Project 1' end end end diff --git a/spec/activerecord-multi-tenant/multi_tenant_spec.rb b/spec/activerecord-multi-tenant/multi_tenant_spec.rb index 89d91e69..0cae7227 100644 --- a/spec/activerecord-multi-tenant/multi_tenant_spec.rb +++ b/spec/activerecord-multi-tenant/multi_tenant_spec.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe MultiTenant do - describe ".load_current_tenant!" do + describe '.load_current_tenant!' do let(:fake_tenant) { OpenStruct.new(id: 1) } let(:mock_klass) { double(find: fake_tenant) } @@ -14,14 +16,14 @@ MultiTenant.default_tenant_class = @original_default_class end - it "sets and returns the loaded current_tenant" do + it 'sets and returns the loaded current_tenant' do expect(mock_klass).to receive(:find).once.with(1) MultiTenant.current_tenant = 1 expect(MultiTenant.load_current_tenant!).to eq(fake_tenant) expect(MultiTenant.current_tenant).to eq(fake_tenant) end - it "respects `.with` lifecycle" do + it 'respects `.with` lifecycle' do expect(mock_klass).to receive(:find).once.with(2) expect(MultiTenant.current_tenant).to eq(nil) MultiTenant.with(2) do @@ -31,34 +33,34 @@ expect(MultiTenant.current_tenant).to eq(nil) end - context "with a loaded current_tenant" do - it "returns the tenant without fetching it" do + context 'with a loaded current_tenant' do + it 'returns the tenant without fetching it' do expect(mock_klass).not_to receive(:find) MultiTenant.current_tenant = fake_tenant expect(MultiTenant.load_current_tenant!).to eq(fake_tenant) end end - context "with a nil current_tenant" do - it "raises an error, as there is not enough information to load the tenant" do + context 'with a nil current_tenant' do + it 'raises an error, as there is not enough information to load the tenant' do expect(mock_klass).not_to receive(:find) - expect { + expect do MultiTenant.load_current_tenant! - }.to raise_error(RuntimeError, 'MultiTenant.current_tenant must be set to load') + end.to raise_error(RuntimeError, 'MultiTenant.current_tenant must be set to load') end end - context "without a default class set" do + context 'without a default class set' do before do MultiTenant.default_tenant_class = nil end - it "raises an error, as there is not enough information to load the tenant" do + it 'raises an error, as there is not enough information to load the tenant' do expect(mock_klass).not_to receive(:find) MultiTenant.current_tenant = 1 - expect { + expect do MultiTenant.load_current_tenant! - }.to raise_error(RuntimeError, 'Only have tenant id, and no default tenant class set') + end.to raise_error(RuntimeError, 'Only have tenant id, and no default tenant class set') end end end diff --git a/spec/activerecord-multi-tenant/query_rewriter_spec.rb b/spec/activerecord-multi-tenant/query_rewriter_spec.rb index 5d5418a8..5bc280cb 100644 --- a/spec/activerecord-multi-tenant/query_rewriter_spec.rb +++ b/spec/activerecord-multi-tenant/query_rewriter_spec.rb @@ -1,112 +1,113 @@ -require 'spec_helper' +# frozen_string_literal: true -describe "Query Rewriter" do +require 'spec_helper' - context "when bulk updating" do - let!(:account) { Account.create!(name: "Test Account") } - let!(:project) { Project.create(name: "Project 1", account: account) } - let!(:manager) { Manager.create(name: "Manager", project: project, account: account) } +describe 'Query Rewriter' do + context 'when bulk updating' do + let!(:account) { Account.create!(name: 'Test Account') } + let!(:project) { Project.create(name: 'Project 1', account: account) } + let!(:manager) { Manager.create(name: 'Manager', project: project, account: account) } - it "updates the records" do - expect { + it 'updates the records' do + expect do MultiTenant.with(account) do - Project.joins(:manager).update_all(name: "New Name") + Project.joins(:manager).update_all(name: 'New Name') end - }.to change { project.reload.name }.from("Project 1").to("New Name") + end.to change { project.reload.name }.from('Project 1').to('New Name') end - it "updates the records without a current tenant" do - expect { - Project.joins(:manager).update_all(name: "New Name") - }.to change { project.reload.name }.from("Project 1").to("New Name") + it 'updates the records without a current tenant' do + expect do + Project.joins(:manager).update_all(name: 'New Name') + end.to change { project.reload.name }.from('Project 1').to('New Name') end - it "update the record" do - expect { + it 'update the record' do + expect do MultiTenant.with(account) do - project.update(name: "New Name") + project.update(name: 'New Name') end - }.to change { project.reload.name }.from("Project 1").to("New Name") + end.to change { project.reload.name }.from('Project 1').to('New Name') end - it "update the record without a current tenant" do - expect { - project.update(name: "New Name") - }.to change { project.reload.name }.from("Project 1").to("New Name") + it 'update the record without a current tenant' do + expect do + project.update(name: 'New Name') + end.to change { project.reload.name }.from('Project 1').to('New Name') end end - context "when bulk deleting" do - let!(:account) { Account.create!(name: "Test Account") } - let!(:project1) { Project.create(name: "Project 1", account: account) } - let!(:project2) { Project.create(name: "Project 2", account: account) } - let!(:project3) { Project.create(name: "Project 3", account: account) } - let!(:manager1) { Manager.create(name: "Manager 1", project: project1, account: account) } - let!(:manager2) { Manager.create(name: "Manager 2", project: project2, account: account) } + context 'when bulk deleting' do + let!(:account) { Account.create!(name: 'Test Account') } + let!(:project1) { Project.create(name: 'Project 1', account: account) } + let!(:project2) { Project.create(name: 'Project 2', account: account) } + let!(:project3) { Project.create(name: 'Project 3', account: account) } + let!(:manager1) { Manager.create(name: 'Manager 1', project: project1, account: account) } + let!(:manager2) { Manager.create(name: 'Manager 2', project: project2, account: account) } - it "delete_all the records" do - expect { + it 'delete_all the records' do + expect do MultiTenant.with(account) do Project.joins(:manager).delete_all end - }.to change { Project.count }.from(3).to(1) + end.to change { Project.count }.from(3).to(1) end - it "delete_all the records without a current tenant" do - expect { + it 'delete_all the records without a current tenant' do + expect do Project.joins(:manager).delete_all - }.to change { Project.count }.from(3).to(1) + end.to change { Project.count }.from(3).to(1) end - it "delete the record" do - expect { + it 'delete the record' do + expect do MultiTenant.with(account) do project1.delete Project.delete(project2.id) end - }.to change { Project.count }.from(3).to(1) + end.to change { Project.count }.from(3).to(1) end - it "delete the record without a current tenant" do - expect { + it 'delete the record without a current tenant' do + expect do project1.delete Project.delete(project2.id) - }.to change { Project.count }.from(3).to(1) + end.to change { Project.count }.from(3).to(1) end - it "destroy the record" do - expect { + it 'destroy the record' do + expect do MultiTenant.with(account) do project1.destroy Project.destroy(project2.id) end - }.to change { Project.count }.from(3).to(1) + end.to change { Project.count }.from(3).to(1) end - it "destroy the record without a current tenant" do - expect { + it 'destroy the record without a current tenant' do + expect do project1.destroy Project.destroy(project2.id) - }.to change { Project.count }.from(3).to(1) + end.to change { Project.count }.from(3).to(1) end end - context "when update without arel" do - it "can call method" do - expect { - ActiveRecord::Base.connection.update("SELECT 1") - }.not_to raise_error + context 'when update without arel' do + it 'can call method' do + expect do + ActiveRecord::Base.connection.update('SELECT 1') + end.not_to raise_error end end - context "when joining with a model with a default scope" do - let!(:account) { Account.create!(name: "Test Account") } + context 'when joining with a model with a default scope' do + let!(:account) { Account.create!(name: 'Test Account') } - it "fetches only records within the default scope" do - alive = Domain.create(name: "alive", account: account) - deleted = Domain.create(name: "deleted", deleted: true, account: account) - page_in_alive_domain = Page.create(name: "alive", account: account, domain: alive) - page_in_deleted_domain = Page.create(name: "deleted", account: account, domain: deleted) + it 'fetches only records within the default scope' do + alive = Domain.create(name: 'alive', account: account) + deleted = Domain.create(name: 'deleted', deleted: true, account: account) + page_in_alive_domain = Page.create(name: 'alive', account: account, domain: alive) + Page.create(name: 'deleted', account: account, domain: deleted) expect( MultiTenant.with(account) do diff --git a/spec/activerecord-multi-tenant/record_finding_spec.rb b/spec/activerecord-multi-tenant/record_finding_spec.rb index 6bda9cd5..99873d1b 100644 --- a/spec/activerecord-multi-tenant/record_finding_spec.rb +++ b/spec/activerecord-multi-tenant/record_finding_spec.rb @@ -61,36 +61,36 @@ end context 'model with has_many relation through multi-tenant model' do - let(:tenant_1) { Account.create! name: 'Tenant 1' } - let(:project_1) { tenant_1.projects.create! } + let(:tenant1) { Account.create! name: 'Tenant 1' } + let(:project1) { tenant1.projects.create! } - let(:tenant_2) { Account.create! name: 'Tenant 2' } - let(:project_2) { tenant_2.projects.create! } + let(:tenant2) { Account.create! name: 'Tenant 2' } + let(:project2) { tenant2.projects.create! } let(:category) { Category.create! name: 'Category' } before do - ProjectCategory.create! account: tenant_1, name: '1', project: project_1, category: category - ProjectCategory.create! account: tenant_2, name: '2', project: project_2, category: category + ProjectCategory.create! account: tenant1, name: '1', project: project1, category: category + ProjectCategory.create! account: tenant2, name: '2', project: project2, category: category end it 'can get model without creating query cache' do - MultiTenant.with(tenant_1) do - found_category = Project.find(project_1.id).categories.to_a.first + MultiTenant.with(tenant1) do + found_category = Project.find(project1.id).categories.to_a.first expect(found_category).to eq(category) end end it 'can get model for other tenant' do - MultiTenant.with(tenant_2) do - found_category = Project.find(project_2.id).categories.to_a.first + MultiTenant.with(tenant2) do + found_category = Project.find(project2.id).categories.to_a.first expect(found_category).to eq(category) end end it 'can get model without current_tenant' do MultiTenant.without do - found_category = Project.find(project_2.id).categories.to_a.first + found_category = Project.find(project2.id).categories.to_a.first expect(found_category).to eq(category) end end diff --git a/spec/activerecord-multi-tenant/record_modifications_spec.rb b/spec/activerecord-multi-tenant/record_modifications_spec.rb index 9e0dcd1c..01423f82 100644 --- a/spec/activerecord-multi-tenant/record_modifications_spec.rb +++ b/spec/activerecord-multi-tenant/record_modifications_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe MultiTenant, 'Record modifications' do @@ -6,7 +8,6 @@ let(:project) { Project.create! name: 'something', account: account } let(:project2) { Project.create! name: 'something2', account: account2, id: project.id } - it 'includes the tenant_id in DELETEs when using object.destroy' do # two records with same id but different account_id # when doing project.destroy it should delete only the current one @@ -16,7 +17,7 @@ expect(project2.account).to eq(account2) expect(project.id).to eq(project2.id) - MultiTenant.without() do + MultiTenant.without do expect(Project.count).to eq(2) project.destroy expect(Project.count).to eq(1) @@ -28,7 +29,6 @@ MultiTenant.with(account2) do expect(Project.where(id: project2.id).first).to be_present end - end it 'includes the tenant_id in DELETEs when using object.delete' do @@ -40,7 +40,7 @@ expect(project2.account).to eq(account2) expect(project.id).to eq(project2.id) - MultiTenant.without() do + MultiTenant.without do expect(Project.count).to eq(2) project.delete expect(Project.count).to eq(1) diff --git a/spec/activerecord-multi-tenant/schema_dumper_tester.rb b/spec/activerecord-multi-tenant/schema_dumper_tester.rb deleted file mode 100644 index e69de29b..00000000 diff --git a/spec/activerecord-multi-tenant/sidekiq_spec.rb b/spec/activerecord-multi-tenant/sidekiq_spec.rb index 20615c5c..b47dee90 100644 --- a/spec/activerecord-multi-tenant/sidekiq_spec.rb +++ b/spec/activerecord-multi-tenant/sidekiq_spec.rb @@ -5,29 +5,29 @@ describe MultiTenant, 'Sidekiq' do let(:server) { Sidekiq::Middleware::MultiTenant::Server.new } let(:account) { Account.create(name: 'test') } - let(:deleted_acount) { Account.create(name: 'deleted') } + let(:deleted_account) { Account.create(name: 'deleted') } - before { deleted_acount.destroy! } + before { deleted_account.destroy! } describe 'server middleware' do it 'sets the multitenant context when provided in message' do - server.call(double,{'bogus' => 'message', - 'multi_tenant' => { 'class' => account.class.name, 'id' => account.id}}, - 'bogus_queue') do + server.call(double, { 'bogus' => 'message', + 'multi_tenant' => { 'class' => account.class.name, 'id' => account.id } }, + 'bogus_queue') do expect(MultiTenant.current_tenant).to eq(account) end end it 'sets the multitenant context (id) even if tenant not found' do - server.call(double,{'bogus' => 'message', - 'multi_tenant' => { 'class' => deleted_acount.class.name, 'id' => deleted_acount.id}}, - 'bogus_queue') do - expect(MultiTenant.current_tenant).to eq(deleted_acount.id) + server.call(double, { 'bogus' => 'message', + 'multi_tenant' => { 'class' => deleted_account.class.name, 'id' => deleted_account.id } }, + 'bogus_queue') do + expect(MultiTenant.current_tenant).to eq(deleted_account.id) end end it 'does not set the multitenant context when no tenant provided' do - server.call(double, {'bogus' => 'message'}, 'bogus_queue') do + server.call(double, { 'bogus' => 'message' }, 'bogus_queue') do expect(MultiTenant.current_tenant).to be_nil end end diff --git a/spec/schema.rb b/spec/schema.rb index ed7d0eec..01578157 100644 --- a/spec/schema.rb +++ b/spec/schema.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Resets the database, except when we are only running a specific spec ARGV.grep(/\w+_spec\.rb/).empty? && ActiveRecord::Schema.define(version: 1) do enable_extension_on_all_nodes 'uuid-ossp' @@ -216,7 +218,7 @@ class SubclassTask < AbstractTask class Comment < ActiveRecord::Base multi_tenant :account belongs_to :commentable, polymorphic: true - belongs_to :task, -> { where(comments: { commentable_type: 'Task' }) }, foreign_key: 'commentable_id' + belongs_to :task, -> { where(comments: { commentable_type: 'Task' }) }, foreign_key: 'commentable_id' end class Organization < ActiveRecord::Base @@ -229,7 +231,7 @@ class UuidRecord < ActiveRecord::Base end class Category < ActiveRecord::Base - has_many :project_categories + has_many :project_categories has_many :projects, through: :project_categories end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5e5cab0d..30ae442d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) $LOAD_PATH.unshift(File.dirname(__FILE__)) @@ -5,19 +7,18 @@ require 'action_controller/railtie' require 'rspec/rails' -require 'activerecord-multi-tenant' +require 'activerecord_multi_tenant' require 'bundler' Bundler.require(:default, :development) require 'simplecov' SimpleCov.start - require 'codecov' SimpleCov.formatter = SimpleCov::Formatter::Codecov -dbconfig = YAML::load(IO.read(File.join(File.dirname(__FILE__), 'database.yml'))) -ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), "debug.log")) +dbconfig = YAML.safe_load(IO.read(File.join(File.dirname(__FILE__), 'database.yml'))) +ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), 'debug.log')) ActiveRecord::Base.establish_connection(dbconfig['test']) RSpec.configure do |config| @@ -31,9 +32,6 @@ config.before(:suite) do MultiTenant::FastTruncate.run - - # Keep this here until https://github.com/citusdata/citus/issues/1236 is fixed - MultiTenant.enable_with_lock_workaround end config.after(:each) do @@ -48,6 +46,8 @@ class Application < Rails::Application; end MultiTenantTest::Application.config.secret_token = 'x' * 40 MultiTenantTest::Application.config.secret_key_base = 'y' * 40 +# rubocop:disable Lint/UnusedMethodArgument +# changing the name of the parameter breaks tests def with_belongs_to_required_by_default(&block) default_value = ActiveRecord::Base.belongs_to_required_by_default ActiveRecord::Base.belongs_to_required_by_default = true @@ -55,4 +55,5 @@ def with_belongs_to_required_by_default(&block) ensure ActiveRecord::Base.belongs_to_required_by_default = default_value end +# rubocop:enable Lint/UnusedMethodArgument require 'schema' From 37c1105e43e59733efcb9ab499aeabc29ec38f71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCrkan=20=C4=B0ndibay?= Date: Tue, 18 Apr 2023 18:42:12 +0300 Subject: [PATCH 075/105] Removes workaround for citus/1236 (#185) Removes workaround for https://github.com/citusdata/citus/issues/1236 * Parameterizes codecov upload to facilitate local development * Adds static code analysis as a separate job * Fixes static code analysis issues * Changes trigger to all branches --- .../active-record-multi-tenant-tests.yml | 24 +++++++++++++------ .../model_extensions.rb | 1 + lib/activerecord-multi-tenant/multi_tenant.rb | 11 --------- lib/activerecord-multi-tenant/with_lock.rb | 23 ------------------ lib/activerecord_multi_tenant.rb | 1 - .../model_extensions_spec.rb | 13 ---------- spec/spec_helper.rb | 12 ++++++---- 7 files changed, 26 insertions(+), 59 deletions(-) delete mode 100644 lib/activerecord-multi-tenant/with_lock.rb diff --git a/.github/workflows/active-record-multi-tenant-tests.yml b/.github/workflows/active-record-multi-tenant-tests.yml index eca8d3b5..5007c7f4 100644 --- a/.github/workflows/active-record-multi-tenant-tests.yml +++ b/.github/workflows/active-record-multi-tenant-tests.yml @@ -1,12 +1,27 @@ name: Active Record Multi-Tenant Tests +env: + CI: true on: push: branches: - - master - pull_request: + - "**" jobs: + + static-checks: + runs-on: ubuntu-latest + steps: + - uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2 + bundler-cache: true + - uses: actions/checkout@v3 + - name: Rubocop static code analysis + run: | + gem install rubocop + rubocop + build: runs-on: ubuntu-latest strategy: @@ -42,11 +57,6 @@ jobs: ruby-version: ${{ matrix.ruby }} bundler-cache: true - - name: Rubocop static code analysis - run: | - gem install rubocop - rubocop - - name: Execute tests run: bundle exec rake spec diff --git a/lib/activerecord-multi-tenant/model_extensions.rb b/lib/activerecord-multi-tenant/model_extensions.rb index bef28561..8e11f79a 100644 --- a/lib/activerecord-multi-tenant/model_extensions.rb +++ b/lib/activerecord-multi-tenant/model_extensions.rb @@ -166,6 +166,7 @@ def inherited(subclass) ActiveRecord::Associations::CollectionProxy.alias_method :<<, :append_mt end +# skips statement caching for classes that is Multi-tenant or has a multi-tenant relation class ActiveRecord::Associations::Association alias skip_statement_cache_orig skip_statement_cache? diff --git a/lib/activerecord-multi-tenant/multi_tenant.rb b/lib/activerecord-multi-tenant/multi_tenant.rb index 090217d5..9c586537 100644 --- a/lib/activerecord-multi-tenant/multi_tenant.rb +++ b/lib/activerecord-multi-tenant/multi_tenant.rb @@ -33,17 +33,6 @@ def self.with_write_only_mode_enabled? @@enable_write_only_mode ||= false end - # Workaroud to make "with_lock" work until https://github.com/citusdata/citus/issues/1236 is fixed - @@enable_with_lock_workaround = false - - def self.enable_with_lock_workaround - @@enable_with_lock_workaround = true - end - - def self.with_lock_workaround_enabled? - @@enable_with_lock_workaround - end - # Registry that maps table names to models (used by the query rewriter) def self.register_multi_tenant_model(model_klass) @@multi_tenant_models ||= [] diff --git a/lib/activerecord-multi-tenant/with_lock.rb b/lib/activerecord-multi-tenant/with_lock.rb deleted file mode 100644 index 82e4a96f..00000000 --- a/lib/activerecord-multi-tenant/with_lock.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -# Workaround for https://github.com/citusdata/citus/issues/1236 -# "SELECT ... FOR UPDATE is not supported for router-plannable queries" - -class ActiveRecord::Base - alias lock_orig lock! - - # rubocop:disable Style/OptionalBooleanParameter - # Having errors when using lock: true - def lock!(lock = true) - if lock && persisted? && self.class.respond_to?(:scoped_by_tenant?) && \ - MultiTenant.current_tenant_id && MultiTenant.with_lock_workaround_enabled? - self.class.unscoped.where(id: id).update_all(id: id) # No-op UPDATE that locks the row - reload # This is just to act similar to the default ActiveRecord approach, in case someone relies on the reload - self - else - lock_orig(lock) - end - end - - # rubocop:enable Style/OptionalBooleanParameter -end diff --git a/lib/activerecord_multi_tenant.rb b/lib/activerecord_multi_tenant.rb index 284461d5..66c6418d 100644 --- a/lib/activerecord_multi_tenant.rb +++ b/lib/activerecord_multi_tenant.rb @@ -9,5 +9,4 @@ require_relative 'activerecord-multi-tenant/query_rewriter' require_relative 'activerecord-multi-tenant/query_monitor' require_relative 'activerecord-multi-tenant/version' -require_relative 'activerecord-multi-tenant/with_lock' require_relative 'activerecord-multi-tenant/string_extension' diff --git a/spec/activerecord-multi-tenant/model_extensions_spec.rb b/spec/activerecord-multi-tenant/model_extensions_spec.rb index cc442064..2474c2c1 100644 --- a/spec/activerecord-multi-tenant/model_extensions_spec.rb +++ b/spec/activerecord-multi-tenant/model_extensions_spec.rb @@ -452,19 +452,6 @@ def self.name end end - describe '.with_lock' do - it 'supports with_lock blocks inside the block' do - @account = Account.create!(name: 'foo') - - MultiTenant.with @account do - project = @account.projects.create!(name: 'project') - project.with_lock do - expect(project.name).to eq 'project' - end - end - end - end - it 'does not cache tenancy in associations' do account1 = Account.create! name: 'test1' account2 = Account.create! name: 'test2' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 30ae442d..4f21ac1a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -12,10 +12,14 @@ require 'bundler' Bundler.require(:default, :development) -require 'simplecov' -SimpleCov.start -require 'codecov' -SimpleCov.formatter = SimpleCov::Formatter::Codecov +# Codecov is enabled when CI is set to true +if ENV['CI'] == 'true' + puts 'Enabling simplecov to upload code coverage results to codecov.io' + require 'simplecov' + SimpleCov.start + require 'codecov' + SimpleCov.formatter = SimpleCov::Formatter::Codecov +end dbconfig = YAML.safe_load(IO.read(File.join(File.dirname(__FILE__), 'database.yml'))) ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), 'debug.log')) From c85fc835db5505ae1d52119f4a12a9c1f051f530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCrkan=20=C4=B0ndibay?= Date: Wed, 3 May 2023 18:30:13 +0300 Subject: [PATCH 076/105] Adds comments to make code more readable (#187) --- .../controller_extensions.rb | 9 ++++++++ .../copy_from_client.rb | 4 ++++ .../fast_truncate.rb | 1 + .../model_extensions.rb | 23 ++++++++++++++++++- lib/activerecord-multi-tenant/multi_tenant.rb | 4 ++++ .../query_monitor.rb | 8 +++++-- .../query_rewriter.rb | 1 + lib/activerecord-multi-tenant/sidekiq.rb | 10 ++++++++ .../string_extension.rb | 2 ++ 9 files changed, 59 insertions(+), 3 deletions(-) diff --git a/lib/activerecord-multi-tenant/controller_extensions.rb b/lib/activerecord-multi-tenant/controller_extensions.rb index b0d7ecf8..cf496561 100644 --- a/lib/activerecord-multi-tenant/controller_extensions.rb +++ b/lib/activerecord-multi-tenant/controller_extensions.rb @@ -1,5 +1,9 @@ # frozen_string_literal: true +# Extension to the controller to allow setting the current tenant +# set_current_tenant and current_tenant methods are introduced +# to set and get the current tenant in the controllers that uses +# the MultiTenant module. module MultiTenant module ControllerExtensions def set_current_tenant_through_filter @@ -22,6 +26,11 @@ def current_tenant end end +# This block is executed when the file is loaded and +# makes the base class; ActionController::Base to +# extend the ControllerExtensions module. +# This will add the set_current_tenant and current_tenant +# in all the controllers that inherit from ActionController::Base ActiveSupport.on_load(:action_controller) do |base| base.extend MultiTenant::ControllerExtensions end diff --git a/lib/activerecord-multi-tenant/copy_from_client.rb b/lib/activerecord-multi-tenant/copy_from_client.rb index eaa468bb..786c7622 100644 --- a/lib/activerecord-multi-tenant/copy_from_client.rb +++ b/lib/activerecord-multi-tenant/copy_from_client.rb @@ -1,4 +1,7 @@ module MultiTenant + # Designed to be mixed into an ActiveRecord model to provide + # a copy_from_client method that allows for efficient bulk insertion of + # data into a PostgreSQL database using the COPY command class CopyFromClientHelper attr_reader :count @@ -28,6 +31,7 @@ def copy_from_client(columns, &block) end end +# Add copy_from_client to ActiveRecord::Base ActiveSupport.on_load(:active_record) do |base| base.extend(MultiTenant::CopyFromClient) end diff --git a/lib/activerecord-multi-tenant/fast_truncate.rb b/lib/activerecord-multi-tenant/fast_truncate.rb index b7db4677..27b61708 100644 --- a/lib/activerecord-multi-tenant/fast_truncate.rb +++ b/lib/activerecord-multi-tenant/fast_truncate.rb @@ -1,5 +1,6 @@ # Truncates only the tables that have been modified, according to sequence # values +# Faster alternative to DatabaseCleaner.clean_with(:truncation, pre_count: true) module MultiTenant module FastTruncate def self.run(exclude: ['schema_migrations']) diff --git a/lib/activerecord-multi-tenant/model_extensions.rb b/lib/activerecord-multi-tenant/model_extensions.rb index 8e11f79a..0ec84f98 100644 --- a/lib/activerecord-multi-tenant/model_extensions.rb +++ b/lib/activerecord-multi-tenant/model_extensions.rb @@ -1,9 +1,18 @@ require_relative './multi_tenant' module MultiTenant + # Extension to the model to allow scoping of models to the current tenant. This is done by adding + # the multitenant method to the models that need to be scoped. This method is called in the + # model declaration. + # Adds scoped_by_tenant? partition_key, primary_key and inherited methods to the model module ModelExtensionsClassMethods DEFAULT_ID_FIELD = 'id'.freeze - + # executes when multi_tenant method is called in the model. This method adds the following + # methods to the model that calls it. + # scoped_by_tenant? - returns true if the model is scoped by tenant + # partition_key - returns the partition key for the model + # primary_key - returns the primary key for the model + # def multi_tenant(tenant_name, options = {}) if to_s.underscore.to_sym == tenant_name || (!table_name.nil? && table_name.singularize.to_sym == tenant_name) unless MultiTenant.with_write_only_mode_enabled? @@ -71,6 +80,12 @@ def inherited(subclass) end } + # Below block adds the following methods to the model that calls it. + # partition_key= - returns the partition key for the model.class << self 'partition' method defined above + # is the getter method. Here, there is additional check to assure that the tenant id is not changed once set + # tenant_name- returns the name of the tenant model. Its setter and getter methods defined separately + # Getter checks for the tenant association and if it is not loaded, returns the current tenant id set + # in the MultiTenant module to_include = Module.new do define_method "#{partition_key}=" do |tenant_id| write_attribute(partition_key.to_s, tenant_id) @@ -110,6 +125,9 @@ def inherited(subclass) end include to_include + # Below blocks sets tenant_id for the current session with the tenant_id of the record + # If the tenant is not set for the `session.After` the save operation current session tenant is set to nil + # If tenant is set for the session, save operation is performed as it is around_save lambda { |record, block| record_tenant = record.attribute_was(partition_key) if persisted? && MultiTenant.current_tenant_id.nil? && !record_tenant.nil? @@ -140,6 +158,9 @@ def inherited(subclass) end end +# Below code block is executed on Model, Associations and CollectionProxy objects +# when ActiveRecord is loaded and decorates defined methods with MultiTenant.with function. +# Additionally, adds aliases for some operators. ActiveSupport.on_load(:active_record) do |base| base.extend MultiTenant::ModelExtensionsClassMethods diff --git a/lib/activerecord-multi-tenant/multi_tenant.rb b/lib/activerecord-multi-tenant/multi_tenant.rb index 9c586537..9d72da68 100644 --- a/lib/activerecord-multi-tenant/multi_tenant.rb +++ b/lib/activerecord-multi-tenant/multi_tenant.rb @@ -122,6 +122,9 @@ def self.without(&block) # 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 + # Instruments the methods provided with previously set Multitenant parameters + # In Ruby 2 using splat (*) operator with `&block` is not supported, so we need to use `method(...)` syntax + # TODO: Could not understand the use of owner here. Need to check if Gem::Version.create(RUBY_VERSION) < Gem::Version.new('3.0.0') def self.wrap_methods(klass, owner, *method_names) method_names.each do |method_name| @@ -159,6 +162,7 @@ def #{method_name}(...) # Preserve backward compatibility for people using .with_id singleton_class.send(:alias_method, :with_id, :with) + # This exception is raised when a there is an attempt to change tenant class TenantIsImmutable < StandardError end end diff --git a/lib/activerecord-multi-tenant/query_monitor.rb b/lib/activerecord-multi-tenant/query_monitor.rb index 627a37aa..85b555f7 100644 --- a/lib/activerecord-multi-tenant/query_monitor.rb +++ b/lib/activerecord-multi-tenant/query_monitor.rb @@ -1,4 +1,7 @@ # Add generic warning when queries fail and there is no tenant set +# To handle this case, a QueryMonitor hook is created and registered +# to sql.active_record. This hook will log a warning when a query fails +# This hook is executed after the query is executed. module MultiTenant # rubocop:disable Style/ClassVars # Option to enable query monitor @@ -13,7 +16,8 @@ def self.query_monitor_enabled? end # rubocop:enable Style/ClassVars - + # QueryMonitor class to log a warning when a query fails and there is no tenant set + # start and finish methods are required to be register sql.active_record hook class QueryMonitor def start(_name, _id, _payload) end @@ -26,5 +30,5 @@ def finish(_name, _id, payload) end end end - +# Actual code to register the hook. ActiveSupport::Notifications.subscribe('sql.active_record', MultiTenant::QueryMonitor.new) diff --git a/lib/activerecord-multi-tenant/query_rewriter.rb b/lib/activerecord-multi-tenant/query_rewriter.rb index adef0dac..da1f3d5d 100644 --- a/lib/activerecord-multi-tenant/query_rewriter.rb +++ b/lib/activerecord-multi-tenant/query_rewriter.rb @@ -1,6 +1,7 @@ require 'active_record' require_relative './arel_visitors_depth_first' unless Arel::Visitors.const_defined?(:DepthFirst) +# Iterates AST and adds tenant enforcement clauses to all relations module MultiTenant class Table attr_reader :arel_table diff --git a/lib/activerecord-multi-tenant/sidekiq.rb b/lib/activerecord-multi-tenant/sidekiq.rb index 9f6b63fd..6bbb5d29 100644 --- a/lib/activerecord-multi-tenant/sidekiq.rb +++ b/lib/activerecord-multi-tenant/sidekiq.rb @@ -1,5 +1,6 @@ require 'sidekiq/client' +# Adds methods to handle tenant information both in the client and server. module Sidekiq::Middleware::MultiTenant # Get the current tenant and store in the message to be sent to Sidekiq. class Client @@ -33,6 +34,8 @@ def call(_worker_class, msg, _queue, &block) end end +# Configure Sidekiq to use the multi-tenant client and server middleware to add (client/server)/process(server) +# tenant information. Sidekiq.configure_server do |config| config.server_middleware do |chain| chain.add Sidekiq::Middleware::MultiTenant::Server @@ -48,8 +51,14 @@ def call(_worker_class, msg, _queue, &block) end end +# Bulk push support for Sidekiq while setting multi-tenant information. +# This is a copy of the Sidekiq::Client#push_bulk method with the addition of +# setting the multi-tenant information for each job. module Sidekiq class Client + # Allows the caller to enqueue multiple Sidekiq jobs with + # tenant information in a single call. It ensures that each job is processed + # within the correct tenant context and returns an array of job IDs for the enqueued jobs def push_bulk_with_tenants(items) first_job = items['jobs'].first return [] unless first_job # no jobs to push @@ -70,6 +79,7 @@ def push_bulk_with_tenants(items) payloads.collect { |payload| payload['jid'] } end + # Enabling the push_bulk_with_tenants method to be called directly on the Sidekiq::Client class class << self def push_bulk_with_tenants(items) new.push_bulk_with_tenants(items) diff --git a/lib/activerecord-multi-tenant/string_extension.rb b/lib/activerecord-multi-tenant/string_extension.rb index b9c04b9d..a4d853fc 100644 --- a/lib/activerecord-multi-tenant/string_extension.rb +++ b/lib/activerecord-multi-tenant/string_extension.rb @@ -2,6 +2,8 @@ require 'anbt-sql-formatter/formatter' +# Formatter for SQL queries. This is used to make the queries more readable in the tests +# When comparing SQL queries in tests, use formatter for both sides of the equation. class String def pretty_format_sql rule = AnbtSql::Rule.new From 4ec77f79e898da8c3143676872c1bcee89517103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCrkan=20=C4=B0ndibay?= Date: Mon, 8 May 2023 18:53:10 +0300 Subject: [PATCH 077/105] Introduces ApplicationRecord (#188) Introduces ApplicationRecord With Rails 5 ApplicationRecord is being used as convention. This commit adds ApplicationRecord as a class and changes all inheritance using ApplicationRecord --- README.md | 4 +- .../application_record.rb | 3 ++ .../copy_from_client.rb | 2 +- .../fast_truncate.rb | 2 +- .../model_extensions.rb | 2 +- .../query_rewriter.rb | 2 +- lib/activerecord_multi_tenant.rb | 1 + .../model_extensions_spec.rb | 4 +- .../query_rewriter_spec.rb | 2 +- .../record_callback_spec.rb | 2 +- spec/schema.rb | 38 +++++++++---------- spec/spec_helper.rb | 10 ++--- 12 files changed, 38 insertions(+), 34 deletions(-) create mode 100644 lib/activerecord-multi-tenant/application_record.rb diff --git a/README.md b/README.md index 3d589cea..92267fb7 100644 --- a/README.md +++ b/README.md @@ -27,14 +27,14 @@ It is required that you add `multi_tenant` definitions to your model in order to In the example of an analytics application, sharding on `customer_id`, annotate your models like this: ```ruby -class PageView < ActiveRecord::Base +class PageView < ApplicationRecord multi_tenant :customer belongs_to :site # ... end -class Site < ActiveRecord::Base +class Site < ApplicationRecord multi_tenant :customer has_many :page_views diff --git a/lib/activerecord-multi-tenant/application_record.rb b/lib/activerecord-multi-tenant/application_record.rb new file mode 100644 index 00000000..10a4cba8 --- /dev/null +++ b/lib/activerecord-multi-tenant/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end diff --git a/lib/activerecord-multi-tenant/copy_from_client.rb b/lib/activerecord-multi-tenant/copy_from_client.rb index 786c7622..a8930b72 100644 --- a/lib/activerecord-multi-tenant/copy_from_client.rb +++ b/lib/activerecord-multi-tenant/copy_from_client.rb @@ -31,7 +31,7 @@ def copy_from_client(columns, &block) end end -# Add copy_from_client to ActiveRecord::Base +# Add copy_from_client to ApplicationRecord ActiveSupport.on_load(:active_record) do |base| base.extend(MultiTenant::CopyFromClient) end diff --git a/lib/activerecord-multi-tenant/fast_truncate.rb b/lib/activerecord-multi-tenant/fast_truncate.rb index 27b61708..4371fc7d 100644 --- a/lib/activerecord-multi-tenant/fast_truncate.rb +++ b/lib/activerecord-multi-tenant/fast_truncate.rb @@ -5,7 +5,7 @@ module MultiTenant module FastTruncate def self.run(exclude: ['schema_migrations']) # This is a slightly faster version of DatabaseCleaner.clean_with(:truncation, pre_count: true) - ActiveRecord::Base.connection.execute format(%( + ApplicationRecord.connection.execute format(%( DO LANGUAGE plpgsql $$ DECLARE t record; diff --git a/lib/activerecord-multi-tenant/model_extensions.rb b/lib/activerecord-multi-tenant/model_extensions.rb index 0ec84f98..f74703f5 100644 --- a/lib/activerecord-multi-tenant/model_extensions.rb +++ b/lib/activerecord-multi-tenant/model_extensions.rb @@ -166,7 +166,7 @@ def inherited(subclass) # Ensure we have current_tenant_id in where clause when a cached ActiveRecord instance is being reloaded, # or update_columns without callbacks is called - MultiTenant.wrap_methods(ActiveRecord::Base, 'self', :delete, :reload, :update_columns) + MultiTenant.wrap_methods(ApplicationRecord, 'self', :delete, :reload, :update_columns) # Any queuries fired for fetching a singular association have the correct current_tenant_id in WHERE clause # reload is called anytime any record's association is accessed diff --git a/lib/activerecord-multi-tenant/query_rewriter.rb b/lib/activerecord-multi-tenant/query_rewriter.rb index da1f3d5d..aeab6545 100644 --- a/lib/activerecord-multi-tenant/query_rewriter.rb +++ b/lib/activerecord-multi-tenant/query_rewriter.rb @@ -380,4 +380,4 @@ def cached_find_by_statement(key, &block) end end -ActiveRecord::Base.singleton_class.prepend(MultiTenantFindBy) +ApplicationRecord.singleton_class.prepend(MultiTenantFindBy) diff --git a/lib/activerecord_multi_tenant.rb b/lib/activerecord_multi_tenant.rb index 66c6418d..3b7eb5f0 100644 --- a/lib/activerecord_multi_tenant.rb +++ b/lib/activerecord_multi_tenant.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require_relative 'activerecord-multi-tenant/application_record' require_relative 'activerecord-multi-tenant/controller_extensions' if Object.const_defined?(:ActionController) require_relative 'activerecord-multi-tenant/copy_from_client' require_relative 'activerecord-multi-tenant/fast_truncate' diff --git a/spec/activerecord-multi-tenant/model_extensions_spec.rb b/spec/activerecord-multi-tenant/model_extensions_spec.rb index 2474c2c1..86b72b47 100644 --- a/spec/activerecord-multi-tenant/model_extensions_spec.rb +++ b/spec/activerecord-multi-tenant/model_extensions_spec.rb @@ -74,7 +74,7 @@ describe 'Tenant model with a nonstandard class name' do let(:account_klass) do - Class.new(ActiveRecord::Base) do + Class.new(ApplicationRecord) do self.table_name = 'account' def self.name @@ -114,7 +114,7 @@ def self.name end let(:post_klass) do - Class.new(ActiveRecord::Base) do + Class.new(ApplicationRecord) do self.table_name = 'unknown' multi_tenant(:account) diff --git a/spec/activerecord-multi-tenant/query_rewriter_spec.rb b/spec/activerecord-multi-tenant/query_rewriter_spec.rb index 5bc280cb..61e23aa1 100644 --- a/spec/activerecord-multi-tenant/query_rewriter_spec.rb +++ b/spec/activerecord-multi-tenant/query_rewriter_spec.rb @@ -95,7 +95,7 @@ context 'when update without arel' do it 'can call method' do expect do - ActiveRecord::Base.connection.update('SELECT 1') + ApplicationRecord.connection.update('SELECT 1') end.not_to raise_error end end diff --git a/spec/activerecord-multi-tenant/record_callback_spec.rb b/spec/activerecord-multi-tenant/record_callback_spec.rb index 4a7e64f0..bff3c96e 100644 --- a/spec/activerecord-multi-tenant/record_callback_spec.rb +++ b/spec/activerecord-multi-tenant/record_callback_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -class ProjectWithCallbacks < ActiveRecord::Base +class ProjectWithCallbacks < ApplicationRecord self.table_name = :projects multi_tenant :account diff --git a/spec/schema.rb b/spec/schema.rb index 01578157..2199b412 100644 --- a/spec/schema.rb +++ b/spec/schema.rb @@ -139,14 +139,14 @@ create_reference_table :categories end -class Account < ActiveRecord::Base +class Account < ApplicationRecord multi_tenant :account has_many :projects has_one :manager, inverse_of: :account has_many :optional_sub_tasks end -class Project < ActiveRecord::Base +class Project < ApplicationRecord multi_tenant :account has_one :manager has_many :tasks @@ -158,12 +158,12 @@ class Project < ActiveRecord::Base validates_uniqueness_of :name, scope: [:account] end -class Manager < ActiveRecord::Base +class Manager < ApplicationRecord multi_tenant :account belongs_to :project end -class Task < ActiveRecord::Base +class Task < ApplicationRecord multi_tenant :account belongs_to :project has_many :sub_tasks @@ -171,7 +171,7 @@ class Task < ActiveRecord::Base validates_uniqueness_of :name end -class SubTask < ActiveRecord::Base +class SubTask < ApplicationRecord multi_tenant :account belongs_to :task has_one :project, through: :task @@ -179,7 +179,7 @@ class SubTask < ActiveRecord::Base end with_belongs_to_required_by_default do - class OptionalSubTask < ActiveRecord::Base + class OptionalSubTask < ApplicationRecord multi_tenant :account, optional: true belongs_to :sub_task end @@ -188,26 +188,26 @@ class OptionalSubTask < ActiveRecord::Base class StiSubTask < SubTask end -class UnscopedModel < ActiveRecord::Base +class UnscopedModel < ApplicationRecord validates_uniqueness_of :name end -class AliasedTask < ActiveRecord::Base +class AliasedTask < ApplicationRecord multi_tenant :account belongs_to :project_alias, class_name: 'Project' end -class CustomPartitionKeyTask < ActiveRecord::Base +class CustomPartitionKeyTask < ApplicationRecord multi_tenant :account, partition_key: 'accountID' validates_uniqueness_of :name, scope: [:account] end -class PartitionKeyNotModelTask < ActiveRecord::Base +class PartitionKeyNotModelTask < ApplicationRecord multi_tenant :non_model end -class AbstractTask < ActiveRecord::Base +class AbstractTask < ApplicationRecord self.abstract_class = true multi_tenant :non_model end @@ -215,44 +215,44 @@ class AbstractTask < ActiveRecord::Base class SubclassTask < AbstractTask end -class Comment < ActiveRecord::Base +class Comment < ApplicationRecord multi_tenant :account belongs_to :commentable, polymorphic: true belongs_to :task, -> { where(comments: { commentable_type: 'Task' }) }, foreign_key: 'commentable_id' end -class Organization < ActiveRecord::Base +class Organization < ApplicationRecord multi_tenant :organization has_many :uuid_records end -class UuidRecord < ActiveRecord::Base +class UuidRecord < ApplicationRecord multi_tenant :organization end -class Category < ActiveRecord::Base +class Category < ApplicationRecord has_many :project_categories has_many :projects, through: :project_categories end -class ProjectCategory < ActiveRecord::Base +class ProjectCategory < ApplicationRecord multi_tenant :account belongs_to :project belongs_to :category belongs_to :account end -class AllowedPlace < ActiveRecord::Base +class AllowedPlace < ApplicationRecord multi_tenant :account end -class Domain < ActiveRecord::Base +class Domain < ApplicationRecord multi_tenant :account has_many :pages default_scope { where(deleted: false) } end -class Page < ActiveRecord::Base +class Page < ApplicationRecord multi_tenant :account belongs_to :domain end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4f21ac1a..39df9740 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -22,8 +22,8 @@ end dbconfig = YAML.safe_load(IO.read(File.join(File.dirname(__FILE__), 'database.yml'))) -ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), 'debug.log')) -ActiveRecord::Base.establish_connection(dbconfig['test']) +ApplicationRecord.logger = Logger.new(File.join(File.dirname(__FILE__), 'debug.log')) +ApplicationRecord.establish_connection(dbconfig['test']) RSpec.configure do |config| config.infer_base_class_for_anonymous_controllers = true @@ -53,11 +53,11 @@ class Application < Rails::Application; end # rubocop:disable Lint/UnusedMethodArgument # changing the name of the parameter breaks tests def with_belongs_to_required_by_default(&block) - default_value = ActiveRecord::Base.belongs_to_required_by_default - ActiveRecord::Base.belongs_to_required_by_default = true + default_value = ApplicationRecord.belongs_to_required_by_default + ApplicationRecord.belongs_to_required_by_default = true yield ensure - ActiveRecord::Base.belongs_to_required_by_default = default_value + ApplicationRecord.belongs_to_required_by_default = default_value end # rubocop:enable Lint/UnusedMethodArgument require 'schema' From 91532c4f44f5c6f83af0a72c89fb2a2f3b7c978a Mon Sep 17 00:00:00 2001 From: alpaca-tc Date: Tue, 9 May 2023 02:06:18 +0900 Subject: [PATCH 078/105] Stop defining methods in global String that are only used for testing. (#189) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Stop defining methods in global * Replace pretty_format_sql with format_sql() * Adds pull request trigger to execute tests --------- Co-authored-by: Gürkan İndibay --- .../active-record-multi-tenant-tests.yml | 2 + lib/activerecord_multi_tenant.rb | 1 - .../model_extensions_spec.rb | 53 ++++++++++--------- spec/spec_helper.rb | 1 + .../support/format_sql.rb | 12 +++-- 5 files changed, 37 insertions(+), 32 deletions(-) rename lib/activerecord-multi-tenant/string_extension.rb => spec/support/format_sql.rb (59%) diff --git a/.github/workflows/active-record-multi-tenant-tests.yml b/.github/workflows/active-record-multi-tenant-tests.yml index 5007c7f4..88219f7d 100644 --- a/.github/workflows/active-record-multi-tenant-tests.yml +++ b/.github/workflows/active-record-multi-tenant-tests.yml @@ -6,6 +6,8 @@ on: push: branches: - "**" + pull_request: + types: [ opened, reopened, synchronize ] jobs: diff --git a/lib/activerecord_multi_tenant.rb b/lib/activerecord_multi_tenant.rb index 3b7eb5f0..98a6e08c 100644 --- a/lib/activerecord_multi_tenant.rb +++ b/lib/activerecord_multi_tenant.rb @@ -10,4 +10,3 @@ require_relative 'activerecord-multi-tenant/query_rewriter' require_relative 'activerecord-multi-tenant/query_monitor' require_relative 'activerecord-multi-tenant/version' -require_relative 'activerecord-multi-tenant/string_extension' diff --git a/spec/activerecord-multi-tenant/model_extensions_spec.rb b/spec/activerecord-multi-tenant/model_extensions_spec.rb index 86b72b47..7a161b58 100644 --- a/spec/activerecord-multi-tenant/model_extensions_spec.rb +++ b/spec/activerecord-multi-tenant/model_extensions_spec.rb @@ -327,7 +327,7 @@ def self.name SQL MultiTenant.with(account) do - expect(Comment.joins(:task).to_sql.pretty_format_sql).to eq(expected_join_sql.pretty_format_sql) + expect(format_sql(Comment.joins(:task).to_sql)).to eq(format_sql(expected_join_sql)) end end @@ -339,7 +339,7 @@ def self.name INNER JOIN "tasks" ON "tasks"."id" = "comments"."commentable_id"#{' '} AND "comments"."commentable_type" = 'Task' AND "comments"."account_id" = "tasks"."account_id" SQL - expect(Comment.joins(:task).to_sql.pretty_format_sql).to eq(expected_join_sql.pretty_format_sql) + expect(format_sql(Comment.joins(:task).to_sql)).to eq(format_sql(expected_join_sql)) end end end @@ -357,7 +357,7 @@ def self.name SQL MultiTenant.with(account) do - expect(Project.joins(:tasks).to_sql.pretty_format_sql).to eq(expected_join_sql.pretty_format_sql) + expect(format_sql(Project.joins(:tasks).to_sql)).to eq(format_sql(expected_join_sql)) end end @@ -369,7 +369,7 @@ def self.name INNER JOIN "tasks" ON "tasks"."project_id" = "projects"."id" AND "projects"."account_id" = "tasks"."account_id" SQL - expect(Project.joins(:tasks).to_sql.pretty_format_sql).to eq(expected_join_sql.pretty_format_sql) + expect(format_sql(Project.joins(:tasks).to_sql)).to eq(format_sql(expected_join_sql)) end end end @@ -494,8 +494,8 @@ def self.name project1 = Project.create! name: 'Project 1' task1 = Task.create! name: 'Task 1', project: project1 subtask1 = SubTask.create! task: task1 - expect(project1.sub_tasks.to_sql.pretty_format_sql) - .to eq(option1.pretty_format_sql).or(eq(option2.pretty_format_sql)) + expect(format_sql(project1.sub_tasks.to_sql)) + .to eq(format_sql(option1)).or(eq(format_sql(option2))) expect(project1.sub_tasks).to include(subtask1) end @@ -509,7 +509,7 @@ def self.name SQL project = Project.first - expect(project.sub_tasks.to_sql.pretty_format_sql).to eq(expected_sql.strip.pretty_format_sql) + expect(format_sql(project.sub_tasks.to_sql)).to eq(format_sql(expected_sql.strip)) end end @@ -534,8 +534,8 @@ def self.name project1 = Project.create! name: 'Project 1' projectcategory = ProjectCategory.create! name: 'project cat 1', project: project1, category: category1 - expect(project1.categories.to_sql.pretty_format_sql) - .to eq(option1.pretty_format_sql).or(eq(option2.pretty_format_sql)) + expect(format_sql(project1.categories.to_sql)) + .to eq(format_sql(option1)).or(eq(format_sql(option2))) expect(project1.categories).to include(category1) expect(project1.project_categories).to include(projectcategory) end @@ -549,8 +549,8 @@ def self.name SQL project = Project.first - expect(project.categories.to_sql.pretty_format_sql) - .to eq(expected_sql.strip.pretty_format_sql) + expect(format_sql(project.categories.to_sql)) + .to eq(format_sql(expected_sql.strip)) expect(project.categories).to include(category1) expected_sql = <<-SQL @@ -561,9 +561,8 @@ def self.name WHERE "projects"."account_id" = 1 SQL - expect(Project.where(account_id: 1).joins(:categories) - .to_sql.pretty_format_sql) - .to eq(expected_sql.strip.pretty_format_sql) + expect(format_sql(Project.where(account_id: 1).joins(:categories).to_sql)) + .to eq(format_sql(expected_sql.strip)) project = Project.where(account_id: 1).joins(:categories).first expect(project.categories).to include(category1) end @@ -599,8 +598,8 @@ def self.name project1 = Project.create! name: 'Project 1' projectcategory = ProjectCategory.create! name: 'project cat 1', project: project1, category: category1 - expect(Project.eager_load(:categories).to_sql.pretty_format_sql) - .to eq(option1.pretty_format_sql).or(eq(option2.pretty_format_sql)) + expect(format_sql(Project.eager_load(:categories).to_sql)) + .to eq(format_sql(option1)).or(eq(format_sql(option2))) project = Project.eager_load(:categories).first expect(project.categories).to include(category1) @@ -618,8 +617,8 @@ def self.name WHERE "projects"."account_id" = 1 SQL - expect(Project.where(account_id: 1).eager_load(:categories).to_sql.pretty_format_sql) - .to eq(expected_sql.strip.pretty_format_sql) + expect(format_sql(Project.where(account_id: 1).eager_load(:categories).to_sql)) + .to eq(format_sql(expected_sql.strip)) project = Project.where(account_id: 1).eager_load(:categories).first expect(project.categories).to include(category1) @@ -649,9 +648,11 @@ def self.name ProjectCategory.create! name: 'project cat 1', project: project1, category: category1 project1.tasks.create! name: 'baz' - expect(Task.joins(:project) - .joins('LEFT JOIN project_categories pc ON project.category_id = pc.id').to_sql.pretty_format_sql) - .to eq(option1.pretty_format_sql).or(eq(option2.pretty_format_sql)) + expect( + format_sql( + Task.joins(:project).joins('LEFT JOIN project_categories pc ON project.category_id = pc.id').to_sql + ) + ).to eq(format_sql(option1)).or(eq(format_sql(option2))) end MultiTenant.without do @@ -663,9 +664,9 @@ def self.name WHERE "tasks"."account_id" = 1 SQL - expect(Task.where(account_id: 1).joins(:project) + expect(format_sql(Task.where(account_id: 1).joins(:project) .joins('LEFT JOIN project_categories pc ON project.category_id = pc.id') - .to_sql.pretty_format_sql).to eq(expected_sql.strip.pretty_format_sql) + .to_sql)).to eq(format_sql(expected_sql.strip)) end end @@ -693,8 +694,8 @@ def self.name # expect(Project).to receive(:find_by_sql).with(eq(option1). # or(eq(option2)).or(eq(option3)), any_args).and_call_original expect(Project).to receive(:find_by_sql).and_wrap_original do |m, *args| - expect(args[0].pretty_format_sql).to(eq(option1.pretty_format_sql) - .or(eq(option2.pretty_format_sql)).or(eq(option3.pretty_format_sql))) + expect(format_sql(args[0])).to(eq(format_sql(option1)) + .or(eq(format_sql(option2))).or(eq(format_sql(option3)))) m.call(args[0], args[1], preparable: args[2][:preparable]) end expect(Project.find(project.id)).to eq(project) @@ -713,7 +714,7 @@ def self.name # Couldn't make the following line pass for some reason, so came up with an uglier alternative # expect(Project).to receive(:find_by_sql).with(eq(option1).or(eq(option2)), any_args).and_call_original expect(Project).to receive(:find_by_sql).and_wrap_original do |m, *args| - expect(args[0].pretty_format_sql).to(eq(option1.pretty_format_sql).or(eq(option2.pretty_format_sql))) + expect(format_sql(args[0])).to(eq(format_sql(option1)).or(eq(format_sql(option2)))) m.call(args[0], args[1], preparable: args[2][:preparable]) end expect(Project.find(project2.id)).to eq(project2) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 39df9740..8c00c981 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -11,6 +11,7 @@ require 'bundler' Bundler.require(:default, :development) +require_relative './support/format_sql' # Codecov is enabled when CI is set to true if ENV['CI'] == 'true' diff --git a/lib/activerecord-multi-tenant/string_extension.rb b/spec/support/format_sql.rb similarity index 59% rename from lib/activerecord-multi-tenant/string_extension.rb rename to spec/support/format_sql.rb index a4d853fc..e9c0e220 100644 --- a/lib/activerecord-multi-tenant/string_extension.rb +++ b/spec/support/format_sql.rb @@ -2,10 +2,8 @@ require 'anbt-sql-formatter/formatter' -# Formatter for SQL queries. This is used to make the queries more readable in the tests -# When comparing SQL queries in tests, use formatter for both sides of the equation. -class String - def pretty_format_sql +module SQLFormatter + def format_sql(sql) rule = AnbtSql::Rule.new rule.keyword = AnbtSql::Rule::KEYWORD_UPPER_CASE %w[count sum substr date].each do |func_name| @@ -13,6 +11,10 @@ def pretty_format_sql end rule.indent_string = ' ' formatter = AnbtSql::Formatter.new(rule) - formatter.format(dup) + formatter.format(sql.dup) end end + +RSpec.configure do |config| + config.include SQLFormatter +end From 5b1f6bc85bcc992ad03d6951ed5c1484fb23d0c5 Mon Sep 17 00:00:00 2001 From: alpaca-tc Date: Tue, 9 May 2023 23:34:07 +0900 Subject: [PATCH 079/105] Modified file name so bundler can automatically require gem (#190) The automatic loading no longer works because the file name has been changed by #184. Bundler automatically call require(gem name) on setup. Therefore, a file matching the gem name must be placed in `$LOAD_PATH`. --- .rubocop.yml | 3 +++ lib/activerecord-multi-tenant.rb | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 lib/activerecord-multi-tenant.rb diff --git a/.rubocop.yml b/.rubocop.yml index 8b4c4380..2d0da73e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -44,3 +44,6 @@ Metrics/CyclomaticComplexity: Metrics/BlockNesting: Enabled: false +Naming/FileName: + Exclude: + - 'lib/activerecord-multi-tenant.rb' diff --git a/lib/activerecord-multi-tenant.rb b/lib/activerecord-multi-tenant.rb new file mode 100644 index 00000000..07fb6f5a --- /dev/null +++ b/lib/activerecord-multi-tenant.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require 'activerecord_multi_tenant' From 5c883258d0e465629c321f42590152a0b2f65520 Mon Sep 17 00:00:00 2001 From: alpaca-tc Date: Tue, 9 May 2023 23:35:17 +0900 Subject: [PATCH 080/105] Revert "Introduces ApplicationRecord (#188)" (#191) This reverts commit 4ec77f79e898da8c3143676872c1bcee89517103. --- README.md | 4 +- .../application_record.rb | 3 -- .../copy_from_client.rb | 2 +- .../fast_truncate.rb | 2 +- .../model_extensions.rb | 2 +- .../query_rewriter.rb | 2 +- lib/activerecord_multi_tenant.rb | 1 - .../model_extensions_spec.rb | 4 +- .../query_rewriter_spec.rb | 2 +- .../record_callback_spec.rb | 2 +- spec/schema.rb | 38 +++++++++---------- spec/spec_helper.rb | 10 ++--- 12 files changed, 34 insertions(+), 38 deletions(-) delete mode 100644 lib/activerecord-multi-tenant/application_record.rb diff --git a/README.md b/README.md index 92267fb7..3d589cea 100644 --- a/README.md +++ b/README.md @@ -27,14 +27,14 @@ It is required that you add `multi_tenant` definitions to your model in order to In the example of an analytics application, sharding on `customer_id`, annotate your models like this: ```ruby -class PageView < ApplicationRecord +class PageView < ActiveRecord::Base multi_tenant :customer belongs_to :site # ... end -class Site < ApplicationRecord +class Site < ActiveRecord::Base multi_tenant :customer has_many :page_views diff --git a/lib/activerecord-multi-tenant/application_record.rb b/lib/activerecord-multi-tenant/application_record.rb deleted file mode 100644 index 10a4cba8..00000000 --- a/lib/activerecord-multi-tenant/application_record.rb +++ /dev/null @@ -1,3 +0,0 @@ -class ApplicationRecord < ActiveRecord::Base - self.abstract_class = true -end diff --git a/lib/activerecord-multi-tenant/copy_from_client.rb b/lib/activerecord-multi-tenant/copy_from_client.rb index a8930b72..786c7622 100644 --- a/lib/activerecord-multi-tenant/copy_from_client.rb +++ b/lib/activerecord-multi-tenant/copy_from_client.rb @@ -31,7 +31,7 @@ def copy_from_client(columns, &block) end end -# Add copy_from_client to ApplicationRecord +# Add copy_from_client to ActiveRecord::Base ActiveSupport.on_load(:active_record) do |base| base.extend(MultiTenant::CopyFromClient) end diff --git a/lib/activerecord-multi-tenant/fast_truncate.rb b/lib/activerecord-multi-tenant/fast_truncate.rb index 4371fc7d..27b61708 100644 --- a/lib/activerecord-multi-tenant/fast_truncate.rb +++ b/lib/activerecord-multi-tenant/fast_truncate.rb @@ -5,7 +5,7 @@ module MultiTenant module FastTruncate def self.run(exclude: ['schema_migrations']) # This is a slightly faster version of DatabaseCleaner.clean_with(:truncation, pre_count: true) - ApplicationRecord.connection.execute format(%( + ActiveRecord::Base.connection.execute format(%( DO LANGUAGE plpgsql $$ DECLARE t record; diff --git a/lib/activerecord-multi-tenant/model_extensions.rb b/lib/activerecord-multi-tenant/model_extensions.rb index f74703f5..0ec84f98 100644 --- a/lib/activerecord-multi-tenant/model_extensions.rb +++ b/lib/activerecord-multi-tenant/model_extensions.rb @@ -166,7 +166,7 @@ def inherited(subclass) # Ensure we have current_tenant_id in where clause when a cached ActiveRecord instance is being reloaded, # or update_columns without callbacks is called - MultiTenant.wrap_methods(ApplicationRecord, 'self', :delete, :reload, :update_columns) + MultiTenant.wrap_methods(ActiveRecord::Base, 'self', :delete, :reload, :update_columns) # Any queuries fired for fetching a singular association have the correct current_tenant_id in WHERE clause # reload is called anytime any record's association is accessed diff --git a/lib/activerecord-multi-tenant/query_rewriter.rb b/lib/activerecord-multi-tenant/query_rewriter.rb index aeab6545..da1f3d5d 100644 --- a/lib/activerecord-multi-tenant/query_rewriter.rb +++ b/lib/activerecord-multi-tenant/query_rewriter.rb @@ -380,4 +380,4 @@ def cached_find_by_statement(key, &block) end end -ApplicationRecord.singleton_class.prepend(MultiTenantFindBy) +ActiveRecord::Base.singleton_class.prepend(MultiTenantFindBy) diff --git a/lib/activerecord_multi_tenant.rb b/lib/activerecord_multi_tenant.rb index 98a6e08c..e4c15191 100644 --- a/lib/activerecord_multi_tenant.rb +++ b/lib/activerecord_multi_tenant.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require_relative 'activerecord-multi-tenant/application_record' require_relative 'activerecord-multi-tenant/controller_extensions' if Object.const_defined?(:ActionController) require_relative 'activerecord-multi-tenant/copy_from_client' require_relative 'activerecord-multi-tenant/fast_truncate' diff --git a/spec/activerecord-multi-tenant/model_extensions_spec.rb b/spec/activerecord-multi-tenant/model_extensions_spec.rb index 7a161b58..2ae94f8a 100644 --- a/spec/activerecord-multi-tenant/model_extensions_spec.rb +++ b/spec/activerecord-multi-tenant/model_extensions_spec.rb @@ -74,7 +74,7 @@ describe 'Tenant model with a nonstandard class name' do let(:account_klass) do - Class.new(ApplicationRecord) do + Class.new(ActiveRecord::Base) do self.table_name = 'account' def self.name @@ -114,7 +114,7 @@ def self.name end let(:post_klass) do - Class.new(ApplicationRecord) do + Class.new(ActiveRecord::Base) do self.table_name = 'unknown' multi_tenant(:account) diff --git a/spec/activerecord-multi-tenant/query_rewriter_spec.rb b/spec/activerecord-multi-tenant/query_rewriter_spec.rb index 61e23aa1..5bc280cb 100644 --- a/spec/activerecord-multi-tenant/query_rewriter_spec.rb +++ b/spec/activerecord-multi-tenant/query_rewriter_spec.rb @@ -95,7 +95,7 @@ context 'when update without arel' do it 'can call method' do expect do - ApplicationRecord.connection.update('SELECT 1') + ActiveRecord::Base.connection.update('SELECT 1') end.not_to raise_error end end diff --git a/spec/activerecord-multi-tenant/record_callback_spec.rb b/spec/activerecord-multi-tenant/record_callback_spec.rb index bff3c96e..4a7e64f0 100644 --- a/spec/activerecord-multi-tenant/record_callback_spec.rb +++ b/spec/activerecord-multi-tenant/record_callback_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -class ProjectWithCallbacks < ApplicationRecord +class ProjectWithCallbacks < ActiveRecord::Base self.table_name = :projects multi_tenant :account diff --git a/spec/schema.rb b/spec/schema.rb index 2199b412..01578157 100644 --- a/spec/schema.rb +++ b/spec/schema.rb @@ -139,14 +139,14 @@ create_reference_table :categories end -class Account < ApplicationRecord +class Account < ActiveRecord::Base multi_tenant :account has_many :projects has_one :manager, inverse_of: :account has_many :optional_sub_tasks end -class Project < ApplicationRecord +class Project < ActiveRecord::Base multi_tenant :account has_one :manager has_many :tasks @@ -158,12 +158,12 @@ class Project < ApplicationRecord validates_uniqueness_of :name, scope: [:account] end -class Manager < ApplicationRecord +class Manager < ActiveRecord::Base multi_tenant :account belongs_to :project end -class Task < ApplicationRecord +class Task < ActiveRecord::Base multi_tenant :account belongs_to :project has_many :sub_tasks @@ -171,7 +171,7 @@ class Task < ApplicationRecord validates_uniqueness_of :name end -class SubTask < ApplicationRecord +class SubTask < ActiveRecord::Base multi_tenant :account belongs_to :task has_one :project, through: :task @@ -179,7 +179,7 @@ class SubTask < ApplicationRecord end with_belongs_to_required_by_default do - class OptionalSubTask < ApplicationRecord + class OptionalSubTask < ActiveRecord::Base multi_tenant :account, optional: true belongs_to :sub_task end @@ -188,26 +188,26 @@ class OptionalSubTask < ApplicationRecord class StiSubTask < SubTask end -class UnscopedModel < ApplicationRecord +class UnscopedModel < ActiveRecord::Base validates_uniqueness_of :name end -class AliasedTask < ApplicationRecord +class AliasedTask < ActiveRecord::Base multi_tenant :account belongs_to :project_alias, class_name: 'Project' end -class CustomPartitionKeyTask < ApplicationRecord +class CustomPartitionKeyTask < ActiveRecord::Base multi_tenant :account, partition_key: 'accountID' validates_uniqueness_of :name, scope: [:account] end -class PartitionKeyNotModelTask < ApplicationRecord +class PartitionKeyNotModelTask < ActiveRecord::Base multi_tenant :non_model end -class AbstractTask < ApplicationRecord +class AbstractTask < ActiveRecord::Base self.abstract_class = true multi_tenant :non_model end @@ -215,44 +215,44 @@ class AbstractTask < ApplicationRecord class SubclassTask < AbstractTask end -class Comment < ApplicationRecord +class Comment < ActiveRecord::Base multi_tenant :account belongs_to :commentable, polymorphic: true belongs_to :task, -> { where(comments: { commentable_type: 'Task' }) }, foreign_key: 'commentable_id' end -class Organization < ApplicationRecord +class Organization < ActiveRecord::Base multi_tenant :organization has_many :uuid_records end -class UuidRecord < ApplicationRecord +class UuidRecord < ActiveRecord::Base multi_tenant :organization end -class Category < ApplicationRecord +class Category < ActiveRecord::Base has_many :project_categories has_many :projects, through: :project_categories end -class ProjectCategory < ApplicationRecord +class ProjectCategory < ActiveRecord::Base multi_tenant :account belongs_to :project belongs_to :category belongs_to :account end -class AllowedPlace < ApplicationRecord +class AllowedPlace < ActiveRecord::Base multi_tenant :account end -class Domain < ApplicationRecord +class Domain < ActiveRecord::Base multi_tenant :account has_many :pages default_scope { where(deleted: false) } end -class Page < ApplicationRecord +class Page < ActiveRecord::Base multi_tenant :account belongs_to :domain end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8c00c981..cde9d6fb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -23,8 +23,8 @@ end dbconfig = YAML.safe_load(IO.read(File.join(File.dirname(__FILE__), 'database.yml'))) -ApplicationRecord.logger = Logger.new(File.join(File.dirname(__FILE__), 'debug.log')) -ApplicationRecord.establish_connection(dbconfig['test']) +ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), 'debug.log')) +ActiveRecord::Base.establish_connection(dbconfig['test']) RSpec.configure do |config| config.infer_base_class_for_anonymous_controllers = true @@ -54,11 +54,11 @@ class Application < Rails::Application; end # rubocop:disable Lint/UnusedMethodArgument # changing the name of the parameter breaks tests def with_belongs_to_required_by_default(&block) - default_value = ApplicationRecord.belongs_to_required_by_default - ApplicationRecord.belongs_to_required_by_default = true + default_value = ActiveRecord::Base.belongs_to_required_by_default + ActiveRecord::Base.belongs_to_required_by_default = true yield ensure - ApplicationRecord.belongs_to_required_by_default = default_value + ActiveRecord::Base.belongs_to_required_by_default = default_value end # rubocop:enable Lint/UnusedMethodArgument require 'schema' From 07163dc8cf0b6811b9e63e3a68034c523d01f439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCrkan=20=C4=B0ndibay?= Date: Mon, 15 May 2023 23:43:43 +0300 Subject: [PATCH 081/105] Adds has_and_belongs_to_many feature with tenant (#193) * Adds has_and_belongs_to_many featurewith tenant * Fixes static code analysis * Parameterizes field name * Parameterizes field name --- lib/activerecord-multi-tenant/habtm.rb | 50 +++++++++++++++++++ lib/activerecord-multi-tenant/multi_tenant.rb | 3 ++ lib/activerecord_multi_tenant.rb | 1 + .../associations_spec.rb | 21 ++++++++ spec/schema.rb | 15 ++++++ 5 files changed, 90 insertions(+) create mode 100644 lib/activerecord-multi-tenant/habtm.rb diff --git a/lib/activerecord-multi-tenant/habtm.rb b/lib/activerecord-multi-tenant/habtm.rb new file mode 100644 index 00000000..8c0edf94 --- /dev/null +++ b/lib/activerecord-multi-tenant/habtm.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +# This module extension is a monkey patch to the ActiveRecord::Associations::ClassMethods module. +# It overrides the has_and_belongs_to_many method to add the tenant_id to the join table if the +# tenant_enabled option is set to true. + +module ActiveRecord + module Associations + module ClassMethods + # rubocop:disable Naming/PredicateName + def has_and_belongs_to_many_with_tenant(name, options = {}, &extension) + # rubocop:enable Naming/PredicateName + has_and_belongs_to_many_without_tenant(name, **options, &extension) + + middle_reflection = _reflections[name.to_s].through_reflection + join_model = middle_reflection.klass + + # get tenant_enabled from options and if it is not set, set it to false + tenant_enabled = options[:tenant_enabled] || false + + return unless tenant_enabled + + tenant_class_name = options[:tenant_class_name] + tenant_column = options[:tenant_column] + + match = tenant_column.match(/(\w+)_id/) + tenant_field_name = match ? match[1] : 'tenant' + + join_model.class_eval do + belongs_to tenant_field_name.to_sym, class_name: tenant_class_name + before_create :tenant_set + + private + + # This method sets the tenant_id on the join table and executes before creation of the join table record. + define_method :tenant_set do + if tenant_enabled + raise MultiTenant::MissingTenantError, 'Tenant Id is not set' unless MultiTenant.current_tenant_id + + send("#{tenant_column}=", MultiTenant.current_tenant_id) + end + end + end + end + + alias has_and_belongs_to_many_without_tenant has_and_belongs_to_many + alias has_and_belongs_to_many has_and_belongs_to_many_with_tenant + end + end +end diff --git a/lib/activerecord-multi-tenant/multi_tenant.rb b/lib/activerecord-multi-tenant/multi_tenant.rb index 9d72da68..b2edb592 100644 --- a/lib/activerecord-multi-tenant/multi_tenant.rb +++ b/lib/activerecord-multi-tenant/multi_tenant.rb @@ -165,4 +165,7 @@ def #{method_name}(...) # This exception is raised when a there is an attempt to change tenant class TenantIsImmutable < StandardError end + + class MissingTenantError < StandardError + end end diff --git a/lib/activerecord_multi_tenant.rb b/lib/activerecord_multi_tenant.rb index e4c15191..8e8e79d5 100644 --- a/lib/activerecord_multi_tenant.rb +++ b/lib/activerecord_multi_tenant.rb @@ -9,3 +9,4 @@ require_relative 'activerecord-multi-tenant/query_rewriter' require_relative 'activerecord-multi-tenant/query_monitor' require_relative 'activerecord-multi-tenant/version' +require_relative 'activerecord-multi-tenant/habtm' diff --git a/spec/activerecord-multi-tenant/associations_spec.rb b/spec/activerecord-multi-tenant/associations_spec.rb index 017aa119..248546b5 100644 --- a/spec/activerecord-multi-tenant/associations_spec.rb +++ b/spec/activerecord-multi-tenant/associations_spec.rb @@ -5,8 +5,11 @@ let(:account2) { Account.create! name: 'test2' } let(:project1) { Project.create! name: 'something1', account: account1 } let(:project2) { Project.create! name: 'something2', account: account2, id: project1.id } + let(:task1) { Task.create! name: 'task1', project: project1, account: account1 } let(:task2) { Task.create! name: 'task2', project: project2, account: account2, id: task1.id } + let(:manager1) { Manager.create! name: 'manager1', account: account1, tasks: [task1] } + let(:project3) { Project.create! name: 'something3', account: account1, managers: [manager1] } context 'include the tenant_id in queries and' do it 'creates a task with correct account_id' do @@ -17,5 +20,23 @@ expect(project2.tasks.count).to eq(1) expect(project2.tasks.first.account_id).to eq(account2.id) # has_many end + + it 'check has_many_belongs_to' do + MultiTenant.with(account1) do + expect(manager1.tasks.first.account_id).to eq(task1.id) # has_many + end + end + + it 'check has_many_belongs_to without tenant in the intermediate table' do + MultiTenant.with(account1) do + expect(manager1.tasks.first.account_id).to eq(task1.id) # has_many + end + end + + it 'check has_many_belongs_to tenant_enabled false' do + MultiTenant.with(account1) do + expect(project3.managers.first.id).to eq(manager1.id) # has_many + end + end end end diff --git a/spec/schema.rb b/spec/schema.rb index 01578157..edc136cb 100644 --- a/spec/schema.rb +++ b/spec/schema.rb @@ -29,6 +29,17 @@ t.column :completed, :boolean end + create_table :managers_tasks, force: true, partition_key: :account_id do |t| + t.column :account_id, :integer + t.column :manager_id, :integer + t.column :task_id, :integer + end + + create_table :managers_projects, force: true do |t| + t.column :project_id, :integer + t.column :manager_id, :integer + end + create_table :sub_tasks, force: true, partition_key: :account_id do |t| t.column :account_id, :integer t.column :name, :string @@ -154,6 +165,7 @@ class Project < ActiveRecord::Base has_many :project_categories has_many :categories, through: :project_categories + has_and_belongs_to_many :managers validates_uniqueness_of :name, scope: [:account] end @@ -161,12 +173,15 @@ class Project < ActiveRecord::Base class Manager < ActiveRecord::Base multi_tenant :account belongs_to :project + has_and_belongs_to_many :tasks, { tenant_column: :account_id, tenant_enabled: true, + tenant_class_name: 'Account' } end class Task < ActiveRecord::Base multi_tenant :account belongs_to :project has_many :sub_tasks + has_and_belongs_to_many :managers, tenant_column: :account_id, tenant_enabled: true validates_uniqueness_of :name end From 99b5af2f25e6d2690e21e89093f3e57d5e6b2519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCrkan=20=C4=B0ndibay?= Date: Thu, 18 May 2023 19:14:47 +0300 Subject: [PATCH 082/105] Fixes code coverage issue (#194) --- .gitignore | 3 +++ .rubocop.yml | 2 ++ Gemfile | 3 ++- spec/spec_helper.rb | 29 ++++++++++++++++++++--------- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 3c01aa0c..b23617df 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ pkg/ Gemfile.lock *.gemfile.lock .idea/ +.vagrant/ +Vagrantfile +coverage/ diff --git a/.rubocop.yml b/.rubocop.yml index 2d0da73e..de3b54a4 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -5,6 +5,8 @@ AllCops: Exclude: - 'vendor/**/*' - 'node_modules/**/*' + - 'Vagrantfile' + TargetRubyVersion: 3.0 Style/FrozenStringLiteralComment: Enabled: false diff --git a/Gemfile b/Gemfile index d0c6aacd..fe65166c 100644 --- a/Gemfile +++ b/Gemfile @@ -2,5 +2,6 @@ source 'https://rubygems.org' gemspec gem 'appraisal' -gem 'codecov', require: false, group: 'test' gem 'rubocop', require: false, group: 'test' +gem 'simplecov' +gem 'simplecov-cobertura' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index cde9d6fb..9ea5c333 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,6 +3,26 @@ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) $LOAD_PATH.unshift(File.dirname(__FILE__)) +# Codecov is enabled when CI is set to true +if ENV['CI'] == 'true' + puts 'Enabling Simplecov to upload code coverage results to codecov.io' + require 'simplecov' + SimpleCov.start 'rails' do + add_filter '/test/' # Exclude test directory from coverage + add_filter '/spec/' # Exclude spec directory from coverage + add_filter '/config/' # Exclude config directory from coverage + + # Add any additional filters or exclusions if needed + # add_filter '/other_directory/' + + add_group 'Lib', '/lib' # Include the lib directory for coverage + puts "Tracked files: #{SimpleCov.tracked_files}" + end + + require 'simplecov-cobertura' + SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter +end + require 'active_record/railtie' require 'action_controller/railtie' require 'rspec/rails' @@ -13,15 +33,6 @@ Bundler.require(:default, :development) require_relative './support/format_sql' -# Codecov is enabled when CI is set to true -if ENV['CI'] == 'true' - puts 'Enabling simplecov to upload code coverage results to codecov.io' - require 'simplecov' - SimpleCov.start - require 'codecov' - SimpleCov.formatter = SimpleCov::Formatter::Codecov -end - dbconfig = YAML.safe_load(IO.read(File.join(File.dirname(__FILE__), 'database.yml'))) ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), 'debug.log')) ActiveRecord::Base.establish_connection(dbconfig['test']) From 2588c0e15fed7b98566c788c3cdf26752a0c5511 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCrkan=20=C4=B0ndibay?= Date: Sat, 27 May 2023 21:24:55 +0300 Subject: [PATCH 083/105] Adds documentation in ReadTheDocs platform (#196) * Adds initial documentation files * Adds remaining documentation files * Updates requirements dependencies * Adds the readthedocs.yml * Updates the documentation * Adds document for APIs * Adds ignores * Adds copy instructions for static files * Adds static files * Adds documentation checks * Adds compilation steps in contributing file --- .../active-record-multi-tenant-tests.yml | 14 + .gitignore | 2 + .readthedocs.yaml | 15 + docs/.gitignore | 3 + docs/Makefile | 28 + docs/api-reference.sh | 10 + docs/requirements.in | 4 + docs/requirements.txt | 62 + .../_static/api-reference/ActiveRecord.html | 130 ++ .../ActiveRecord/Associations.html | 117 ++ .../Associations/Association.html | 285 ++++ .../Associations/ClassMethods.html | 255 +++ .../ActiveRecord/ConnectionAdapters.html | 126 ++ .../ConnectionAdapters/SchemaStatements.html | 232 +++ .../ActiveRecord/QueryMethods.html | 336 ++++ .../ActiveRecord/SchemaDumper.html | 121 ++ .../_static/api-reference/MultiTenant.html | 1454 +++++++++++++++++ .../MultiTenant/ArelTenantVisitor.html | 755 +++++++++ .../MultiTenant/ArelVisitorsDepthFirst.html | 208 +++ .../BaseTenantEnforcementClause.html | 462 ++++++ .../api-reference/MultiTenant/Context.html | 659 ++++++++ .../MultiTenant/ControllerExtensions.html | 202 +++ .../MultiTenant/CopyFromClient.html | 186 +++ .../MultiTenant/CopyFromClientHelper.html | 362 ++++ .../api-reference/MultiTenant/Current.html | 124 ++ .../MultiTenant/DatabaseStatements.html | 366 +++++ .../MultiTenant/FastTruncate.html | 226 +++ .../MultiTenant/MigrationExtensions.html | 554 +++++++ .../MultiTenant/MissingTenantError.html | 124 ++ .../ModelExtensionsClassMethods.html | 492 ++++++ .../MultiTenant/QueryMonitor.html | 257 +++ .../api-reference/MultiTenant/Table.html | 419 +++++ .../MultiTenant/TenantEnforcementClause.html | 148 ++ .../MultiTenant/TenantIsImmutable.html | 135 ++ .../TenantJoinEnforcementClause.html | 310 ++++ .../MultiTenant/TenantValueVisitor.html | 239 +++ .../api-reference/MultiTenantFindBy.html | 180 ++ .../source/_static/api-reference/Sidekiq.html | 126 ++ .../_static/api-reference/Sidekiq/Client.html | 302 ++++ .../Sidekiq/Middleware/MultiTenant.html | 126 ++ .../Middleware/MultiTenant/Client.html | 217 +++ .../Middleware/MultiTenant/Server.html | 219 +++ docs/source/_static/api-reference/_index.html | 399 +++++ .../_static/api-reference/class_list.html | 51 + .../_static/api-reference/css/common.css | 1 + .../_static/api-reference/css/full_list.css | 58 + .../_static/api-reference/css/style.css | 497 ++++++ .../_static/api-reference/file.README.html | 167 ++ .../_static/api-reference/file_list.html | 56 + docs/source/_static/api-reference/frames.html | 17 + docs/source/_static/api-reference/index.html | 167 ++ docs/source/_static/api-reference/js/app.js | 314 ++++ .../_static/api-reference/js/full_list.js | 216 +++ .../source/_static/api-reference/js/jquery.js | 4 + .../_static/api-reference/method_list.html | 715 ++++++++ .../api-reference/top-level-namespace.html | 126 ++ docs/source/_templates/.gitignore | 4 + docs/source/api-reference.rst | 8 + docs/source/appendix.rst | 26 + docs/source/changelog.rst | 8 + docs/source/community-and-support.rst | 26 + docs/source/conf.py | 30 + docs/source/contributing.rst | 70 + docs/source/getting-started.rst | 37 + docs/source/guides-and-tutorials.rst | 129 ++ docs/source/index.rst | 51 + docs/source/introduction.rst | 33 + docs/source/license.rst | 22 + docs/source/troubleshooting.rst | 41 + docs/source/usage-guide.rst | 59 + .../associations_spec.rb | 4 +- 71 files changed, 13926 insertions(+), 2 deletions(-) create mode 100644 .readthedocs.yaml create mode 100644 docs/.gitignore create mode 100644 docs/Makefile create mode 100644 docs/api-reference.sh create mode 100644 docs/requirements.in create mode 100644 docs/requirements.txt create mode 100644 docs/source/_static/api-reference/ActiveRecord.html create mode 100644 docs/source/_static/api-reference/ActiveRecord/Associations.html create mode 100644 docs/source/_static/api-reference/ActiveRecord/Associations/Association.html create mode 100644 docs/source/_static/api-reference/ActiveRecord/Associations/ClassMethods.html create mode 100644 docs/source/_static/api-reference/ActiveRecord/ConnectionAdapters.html create mode 100644 docs/source/_static/api-reference/ActiveRecord/ConnectionAdapters/SchemaStatements.html create mode 100644 docs/source/_static/api-reference/ActiveRecord/QueryMethods.html create mode 100644 docs/source/_static/api-reference/ActiveRecord/SchemaDumper.html create mode 100644 docs/source/_static/api-reference/MultiTenant.html create mode 100644 docs/source/_static/api-reference/MultiTenant/ArelTenantVisitor.html create mode 100644 docs/source/_static/api-reference/MultiTenant/ArelVisitorsDepthFirst.html create mode 100644 docs/source/_static/api-reference/MultiTenant/BaseTenantEnforcementClause.html create mode 100644 docs/source/_static/api-reference/MultiTenant/Context.html create mode 100644 docs/source/_static/api-reference/MultiTenant/ControllerExtensions.html create mode 100644 docs/source/_static/api-reference/MultiTenant/CopyFromClient.html create mode 100644 docs/source/_static/api-reference/MultiTenant/CopyFromClientHelper.html create mode 100644 docs/source/_static/api-reference/MultiTenant/Current.html create mode 100644 docs/source/_static/api-reference/MultiTenant/DatabaseStatements.html create mode 100644 docs/source/_static/api-reference/MultiTenant/FastTruncate.html create mode 100644 docs/source/_static/api-reference/MultiTenant/MigrationExtensions.html create mode 100644 docs/source/_static/api-reference/MultiTenant/MissingTenantError.html create mode 100644 docs/source/_static/api-reference/MultiTenant/ModelExtensionsClassMethods.html create mode 100644 docs/source/_static/api-reference/MultiTenant/QueryMonitor.html create mode 100644 docs/source/_static/api-reference/MultiTenant/Table.html create mode 100644 docs/source/_static/api-reference/MultiTenant/TenantEnforcementClause.html create mode 100644 docs/source/_static/api-reference/MultiTenant/TenantIsImmutable.html create mode 100644 docs/source/_static/api-reference/MultiTenant/TenantJoinEnforcementClause.html create mode 100644 docs/source/_static/api-reference/MultiTenant/TenantValueVisitor.html create mode 100644 docs/source/_static/api-reference/MultiTenantFindBy.html create mode 100644 docs/source/_static/api-reference/Sidekiq.html create mode 100644 docs/source/_static/api-reference/Sidekiq/Client.html create mode 100644 docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant.html create mode 100644 docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant/Client.html create mode 100644 docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant/Server.html create mode 100644 docs/source/_static/api-reference/_index.html create mode 100644 docs/source/_static/api-reference/class_list.html create mode 100644 docs/source/_static/api-reference/css/common.css create mode 100644 docs/source/_static/api-reference/css/full_list.css create mode 100644 docs/source/_static/api-reference/css/style.css create mode 100644 docs/source/_static/api-reference/file.README.html create mode 100644 docs/source/_static/api-reference/file_list.html create mode 100644 docs/source/_static/api-reference/frames.html create mode 100644 docs/source/_static/api-reference/index.html create mode 100644 docs/source/_static/api-reference/js/app.js create mode 100644 docs/source/_static/api-reference/js/full_list.js create mode 100644 docs/source/_static/api-reference/js/jquery.js create mode 100644 docs/source/_static/api-reference/method_list.html create mode 100644 docs/source/_static/api-reference/top-level-namespace.html create mode 100644 docs/source/_templates/.gitignore create mode 100644 docs/source/api-reference.rst create mode 100644 docs/source/appendix.rst create mode 100644 docs/source/changelog.rst create mode 100644 docs/source/community-and-support.rst create mode 100644 docs/source/conf.py create mode 100644 docs/source/contributing.rst create mode 100644 docs/source/getting-started.rst create mode 100644 docs/source/guides-and-tutorials.rst create mode 100644 docs/source/index.rst create mode 100644 docs/source/introduction.rst create mode 100644 docs/source/license.rst create mode 100644 docs/source/troubleshooting.rst create mode 100644 docs/source/usage-guide.rst diff --git a/.github/workflows/active-record-multi-tenant-tests.yml b/.github/workflows/active-record-multi-tenant-tests.yml index 88219f7d..1d7e09c2 100644 --- a/.github/workflows/active-record-multi-tenant-tests.yml +++ b/.github/workflows/active-record-multi-tenant-tests.yml @@ -23,6 +23,20 @@ jobs: run: | gem install rubocop rubocop + doc_checks: + runs-on: ubuntu-latest + steps: + - uses: actions/setup-python@v4 + with: + python-version: 3.9 + - uses: actions/checkout@v3 + - name: Install python dependencies + run: | + pip install -r docs/requirements.txt + - name: Documentation Checks + run: | + cd docs + sphinx-build -W -b html source builds build: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index b23617df..b600ebfc 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ Gemfile.lock .vagrant/ Vagrantfile coverage/ +docs/build/ +.yardoc/ \ No newline at end of file diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..688e898b --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,15 @@ +version: 2 + +build: + os: "ubuntu-22.04" + tools: + python: "3.11" + +# Build from the docs/ directory with Sphinx +sphinx: + configuration: docs/source/conf.py + +# Explicitly set the version of Python and its requirements +python: + install: + - requirements: docs/requirements.txt diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..394d0449 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,3 @@ +# Ignore everything in this directory +.yardoc/* + diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..a2e58bba --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,28 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# New target that executes the bash script before calling the original target +build-docs: pre-build # Change 'html' to the target you want to execute + +pre-build: + # Execute your bash script here + bash api-reference.sh + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +html: + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + diff --git a/docs/api-reference.sh b/docs/api-reference.sh new file mode 100644 index 00000000..1be5a3b4 --- /dev/null +++ b/docs/api-reference.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# This script is used to generate the API reference documentation for the +# project. It is intended to be run from the root of the project directory. +# It requires the following tools to be installed: +# - yard (gem install yard) +echo "Generating API reference documentation..." +echo "Pwd: $(pwd)" +cd .. +yard doc --output-dir docs/source/_static/api-reference diff --git a/docs/requirements.in b/docs/requirements.in new file mode 100644 index 00000000..da3e99a3 --- /dev/null +++ b/docs/requirements.in @@ -0,0 +1,4 @@ +sphinxnotes-strike +sphinx +sphinx_rtd_theme +readthedocs-sphinx-search \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..296e53c1 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,62 @@ +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile requirements.in +# +alabaster==0.7.13 + # via sphinx +babel==2.12.1 + # via sphinx +certifi==2023.5.7 + # via requests +charset-normalizer==3.1.0 + # via requests +docutils==0.18.1 + # via + # sphinx + # sphinx-rtd-theme +idna==3.4 + # via requests +imagesize==1.4.1 + # via sphinx +jinja2==3.1.2 + # via sphinx +markupsafe==2.1.2 + # via jinja2 +packaging==23.1 + # via sphinx +pygments==2.15.1 + # via sphinx +readthedocs-sphinx-search==0.3.1 + # via -r requirements.in +requests==2.31.0 + # via sphinx +snowballstemmer==2.2.0 + # via sphinx +sphinx==6.2.1 + # via + # -r requirements.in + # sphinx-rtd-theme + # sphinxcontrib-jquery + # sphinxnotes-strike +sphinx-rtd-theme==1.2.1 + # via -r requirements.in +sphinxcontrib-applehelp==1.0.4 + # via sphinx +sphinxcontrib-devhelp==1.0.2 + # via sphinx +sphinxcontrib-htmlhelp==2.0.1 + # via sphinx +sphinxcontrib-jquery==4.1 + # via sphinx-rtd-theme +sphinxcontrib-jsmath==1.0.1 + # via sphinx +sphinxcontrib-qthelp==1.0.3 + # via sphinx +sphinxcontrib-serializinghtml==1.1.5 + # via sphinx +sphinxnotes-strike==1.2 + # via -r requirements.in +urllib3==2.0.2 + # via requests diff --git a/docs/source/_static/api-reference/ActiveRecord.html b/docs/source/_static/api-reference/ActiveRecord.html new file mode 100644 index 00000000..2e8e5584 --- /dev/null +++ b/docs/source/_static/api-reference/ActiveRecord.html @@ -0,0 +1,130 @@ + + + + + + + Module: ActiveRecord + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: ActiveRecord + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/habtm.rb,
+ lib/activerecord-multi-tenant/query_rewriter.rb,
lib/activerecord-multi-tenant/migrations.rb,
lib/activerecord-multi-tenant/migrations.rb
+
+
+ +
+ +

Overview

+
+ +

This module extension is a monkey patch to the ActiveRecord::Associations::ClassMethods module. It overrides the has_and_belongs_to_many method to add the tenant_id to the join table if the tenant_enabled option is set to true.

+ + +
+
+
+ + +

Defined Under Namespace

+

+ + + Modules: Associations, ConnectionAdapters, QueryMethods + + + + Classes: SchemaDumper + + +

+ + + + + + + + + +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/ActiveRecord/Associations.html b/docs/source/_static/api-reference/ActiveRecord/Associations.html new file mode 100644 index 00000000..018e5f71 --- /dev/null +++ b/docs/source/_static/api-reference/ActiveRecord/Associations.html @@ -0,0 +1,117 @@ + + + + + + + Module: ActiveRecord::Associations + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: ActiveRecord::Associations + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/habtm.rb
+
+ +
+ +

Defined Under Namespace

+

+ + + Modules: ClassMethods + + + + Classes: Association + + +

+ + + + + + + + + +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/ActiveRecord/Associations/Association.html b/docs/source/_static/api-reference/ActiveRecord/Associations/Association.html new file mode 100644 index 00000000..8818d1e5 --- /dev/null +++ b/docs/source/_static/api-reference/ActiveRecord/Associations/Association.html @@ -0,0 +1,285 @@ + + + + + + + Class: ActiveRecord::Associations::Association + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: ActiveRecord::Associations::Association + + + +

+
+ +
+
Inherits:
+
+ Object + +
    +
  • Object
  • + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/model_extensions.rb
+
+ +
+ +

Overview

+
+ +

skips statement caching for classes that is Multi-tenant or has a multi-tenant relation

+ + +
+
+
+ + +
+ + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Instance Method Details

+ + +
+

+ + #skip_statement_cache?(*scope) ⇒ Boolean + + + + + +

+
+ + +
+
+
+ +

Returns:

+
    + +
  • + + + (Boolean) + + + +
  • + +
+ +
+ + + + +
+
+
+
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+
+
# File 'lib/activerecord-multi-tenant/model_extensions.rb', line 194
+
+def skip_statement_cache?(*scope)
+  return true if klass.respond_to?(:scoped_by_tenant?) && klass.scoped_by_tenant?
+
+  if reflection.through_reflection
+    through_klass = reflection.through_reflection.klass
+    return true if through_klass.respond_to?(:scoped_by_tenant?) && through_klass.scoped_by_tenant?
+  end
+
+  skip_statement_cache_orig(*scope)
+end
+
+
+ +
+

+ + #skip_statement_cache_origObject + + + + + +

+ + + + +
+
+
+
+192
+
+
# File 'lib/activerecord-multi-tenant/model_extensions.rb', line 192
+
+alias skip_statement_cache_orig skip_statement_cache?
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/ActiveRecord/Associations/ClassMethods.html b/docs/source/_static/api-reference/ActiveRecord/Associations/ClassMethods.html new file mode 100644 index 00000000..2d1785ed --- /dev/null +++ b/docs/source/_static/api-reference/ActiveRecord/Associations/ClassMethods.html @@ -0,0 +1,255 @@ + + + + + + + Module: ActiveRecord::Associations::ClassMethods + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: ActiveRecord::Associations::ClassMethods + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/habtm.rb
+
+ +
+ + + + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Instance Method Details

+ + +
+

+ + #has_and_belongs_to_many_with_tenant(name, options = {}, &extension) ⇒ Object + + + + Also known as: + has_and_belongs_to_many + + + + +

+
+ +

rubocop:disable Naming/PredicateName

+ + +
+
+
+ + +
+ + + + +
+
+
+
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+
+
# File 'lib/activerecord-multi-tenant/habtm.rb', line 11
+
+def has_and_belongs_to_many_with_tenant(name, options = {}, &extension)
+  # rubocop:enable Naming/PredicateName
+  has_and_belongs_to_many_without_tenant(name, **options, &extension)
+
+  middle_reflection = _reflections[name.to_s].through_reflection
+  join_model = middle_reflection.klass
+
+  # get tenant_enabled from options and if it is not set, set it to false
+  tenant_enabled = options[:tenant_enabled] || false
+
+  return unless tenant_enabled
+
+  tenant_class_name = options[:tenant_class_name]
+  tenant_column = options[:tenant_column]
+
+  match = tenant_column.match(/(\w+)_id/)
+  tenant_field_name = match ? match[1] : 'tenant'
+
+  join_model.class_eval do
+    belongs_to tenant_field_name.to_sym, class_name: tenant_class_name
+    before_create :tenant_set
+
+    private
+
+    # This method sets the tenant_id on the join table and executes before creation of the join table record.
+    define_method :tenant_set do
+      if tenant_enabled
+        raise MultiTenant::MissingTenantError, 'Tenant Id is not set' unless MultiTenant.current_tenant_id
+
+        send("#{tenant_column}=", MultiTenant.current_tenant_id)
+      end
+    end
+  end
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/ActiveRecord/ConnectionAdapters.html b/docs/source/_static/api-reference/ActiveRecord/ConnectionAdapters.html new file mode 100644 index 00000000..25a67f99 --- /dev/null +++ b/docs/source/_static/api-reference/ActiveRecord/ConnectionAdapters.html @@ -0,0 +1,126 @@ + + + + + + + Module: ActiveRecord::ConnectionAdapters + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: ActiveRecord::ConnectionAdapters + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/migrations.rb
+
+ +
+ +

Overview

+
+ +

:nodoc:

+ + +
+
+
+ + +

Defined Under Namespace

+

+ + + Modules: SchemaStatements + + + + +

+ + + + + + + + + +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/ActiveRecord/ConnectionAdapters/SchemaStatements.html b/docs/source/_static/api-reference/ActiveRecord/ConnectionAdapters/SchemaStatements.html new file mode 100644 index 00000000..5d6f36e8 --- /dev/null +++ b/docs/source/_static/api-reference/ActiveRecord/ConnectionAdapters/SchemaStatements.html @@ -0,0 +1,232 @@ + + + + + + + Module: ActiveRecord::ConnectionAdapters::SchemaStatements + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: ActiveRecord::ConnectionAdapters::SchemaStatements + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/migrations.rb
+
+ +
+ + + + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Instance Method Details

+ + +
+

+ + #create_table(table_name, options = {}, &block) ⇒ Object + + + + + +

+ + + + +
+
+
+
+74
+75
+76
+77
+78
+79
+80
+81
+
+
# File 'lib/activerecord-multi-tenant/migrations.rb', line 74
+
+def create_table(table_name, options = {}, &block)
+  ret = orig_create_table(table_name, **options.except(:partition_key), &block)
+  if options[:id] != false && options[:partition_key] && options[:partition_key].to_s != 'id'
+    execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{table_name}_pkey"
+    execute "ALTER TABLE #{table_name} ADD PRIMARY KEY(\"#{options[:partition_key]}\", id)"
+  end
+  ret
+end
+
+
+ +
+

+ + #orig_create_tableObject + + + + + +

+ + + + +
+
+
+
+72
+
+
# File 'lib/activerecord-multi-tenant/migrations.rb', line 72
+
+alias orig_create_table create_table
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/ActiveRecord/QueryMethods.html b/docs/source/_static/api-reference/ActiveRecord/QueryMethods.html new file mode 100644 index 00000000..f0ad0103 --- /dev/null +++ b/docs/source/_static/api-reference/ActiveRecord/QueryMethods.html @@ -0,0 +1,336 @@ + + + + + + + Module: ActiveRecord::QueryMethods + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: ActiveRecord::QueryMethods + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/query_rewriter.rb
+
+ +
+ + + + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Instance Method Details

+ + +
+

+ + #build_arel(*args) ⇒ Object + + + + + +

+ + + + +
+
+
+
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 288
+
+def build_arel(*args)
+  arel = build_arel_orig(*args)
+
+  unless MultiTenant.with_write_only_mode_enabled?
+    visitor = MultiTenant::ArelTenantVisitor.new(arel)
+
+    visitor.contexts.each do |context|
+      node = context.arel_node
+
+      context.unhandled_relations.each do |relation|
+        model = MultiTenant.multi_tenant_model_for_table(relation.arel_table.table_name)
+
+        if MultiTenant.current_tenant_id
+          enforcement_clause = MultiTenant::TenantEnforcementClause.new(relation.arel_table[model.partition_key])
+          case node
+          when Arel::Nodes::Join # Arel::Nodes::OuterJoin, Arel::Nodes::RightOuterJoin, Arel::Nodes::FullOuterJoin
+            node.right.expr = node.right.expr.and(enforcement_clause)
+          when Arel::Nodes::SelectCore
+            if node.wheres.empty?
+              node.wheres = [enforcement_clause]
+            elsif node.wheres[0].is_a?(Arel::Nodes::And)
+              node.wheres[0].children << enforcement_clause
+            else
+              node.wheres[0] = enforcement_clause.and(node.wheres[0])
+            end
+          else
+            raise 'UnknownContext'
+          end
+        end
+
+        next unless node.is_a?(Arel::Nodes::SelectCore) || node.is_a?(Arel::Nodes::Join)
+
+        node_list = if node.is_a? Arel::Nodes::Join
+                      [node]
+                    else
+                      node.source.right
+                    end
+
+        node_list.select { |n| n.is_a? Arel::Nodes::Join }.each do |node_join|
+          next unless node_join.right
+
+          relation_right, relation_left = relations_from_node_join(node_join)
+
+          next unless relation_right && relation_left
+
+          model_right = MultiTenant.multi_tenant_model_for_table(relation_left.table_name)
+          model_left = MultiTenant.multi_tenant_model_for_table(relation_right.table_name)
+          next unless model_right && model_left
+
+          join_enforcement_clause = MultiTenant::TenantJoinEnforcementClause.new(
+            relation_right[model_right.partition_key], relation_left
+          )
+          node_join.right.expr = node_join.right.expr.and(join_enforcement_clause)
+        end
+      end
+    end
+  end
+
+  arel
+end
+
+
+ +
+

+ + #build_arel_origObject + + + + + +

+ + + + +
+
+
+
+286
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 286
+
+alias build_arel_orig build_arel
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/ActiveRecord/SchemaDumper.html b/docs/source/_static/api-reference/ActiveRecord/SchemaDumper.html new file mode 100644 index 00000000..71845cbc --- /dev/null +++ b/docs/source/_static/api-reference/ActiveRecord/SchemaDumper.html @@ -0,0 +1,121 @@ + + + + + + + Class: ActiveRecord::SchemaDumper + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: ActiveRecord::SchemaDumper + + + +

+
+ +
+
Inherits:
+
+ Object + +
    +
  • Object
  • + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/migrations.rb
+
+ +
+ + + + + + + + + + +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant.html b/docs/source/_static/api-reference/MultiTenant.html new file mode 100644 index 00000000..91cf13a5 --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant.html @@ -0,0 +1,1454 @@ + + + + + + + Module: MultiTenant + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: MultiTenant + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/fast_truncate.rb,
+ lib/activerecord-multi-tenant/version.rb,
lib/activerecord-multi-tenant/migrations.rb,
lib/activerecord-multi-tenant/multi_tenant.rb,
lib/activerecord-multi-tenant/query_monitor.rb,
lib/activerecord-multi-tenant/query_rewriter.rb,
lib/activerecord-multi-tenant/copy_from_client.rb,
lib/activerecord-multi-tenant/model_extensions.rb,
lib/activerecord-multi-tenant/controller_extensions.rb,
lib/activerecord-multi-tenant/arel_visitors_depth_first.rb
+
+
+ +
+ +

Overview

+
+ +

Extension to the controller to allow setting the current tenant set_current_tenant and current_tenant methods are introduced to set and get the current tenant in the controllers that uses the MultiTenant module.

+ + +
+
+
+ + +

Defined Under Namespace

+

+ + + Modules: ControllerExtensions, CopyFromClient, DatabaseStatements, FastTruncate, MigrationExtensions, ModelExtensionsClassMethods, TenantValueVisitor + + + + Classes: ArelTenantVisitor, ArelVisitorsDepthFirst, BaseTenantEnforcementClause, Context, CopyFromClientHelper, Current, MissingTenantError, QueryMonitor, Table, TenantEnforcementClause, TenantIsImmutable, TenantJoinEnforcementClause + + +

+ + +

+ Constant Summary + collapse +

+ +
+ +
VERSION = + +
+
'2.2.0'.freeze
+ +
@@enable_query_monitor = +
+
+ +

rubocop:disable Style/ClassVars Option to enable query monitor

+ + +
+
+
+ + +
+
+
false
+ +
+ + + + + + + + + +

+ Class Method Summary + collapse +

+ + + + + + +
+

Class Method Details

+ + +
+

+ + .current_tenantObject + + + + + +

+ + + + +
+
+
+
+71
+72
+73
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 71
+
+def self.current_tenant
+  Current.tenant
+end
+
+
+ +
+

+ + .current_tenant=(tenant) ⇒ Object + + + + + +

+ + + + +
+
+
+
+67
+68
+69
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 67
+
+def self.current_tenant=(tenant)
+  Current.tenant = tenant
+end
+
+
+ +
+

+ + .current_tenant_classObject + + + + + +

+ + + + +
+
+
+
+83
+84
+85
+86
+87
+88
+89
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 83
+
+def self.current_tenant_class
+  if current_tenant_is_id?
+    MultiTenant.default_tenant_class || raise('Only have tenant id, and no default tenant class set')
+  elsif current_tenant
+    MultiTenant.current_tenant.class.name
+  end
+end
+
+
+ +
+

+ + .current_tenant_idObject + + + + + +

+ + + + +
+
+
+
+75
+76
+77
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 75
+
+def self.current_tenant_id
+  current_tenant_is_id? ? current_tenant : current_tenant.try(:id)
+end
+
+
+ +
+

+ + .current_tenant_is_id?Boolean + + + + + +

+
+ + +
+
+
+ +

Returns:

+
    + +
  • + + + (Boolean) + + + +
  • + +
+ +
+ + + + +
+
+
+
+79
+80
+81
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 79
+
+def self.current_tenant_is_id?
+  current_tenant.is_a?(String) || current_tenant.is_a?(Integer)
+end
+
+
+ +
+

+ + .default_tenant_classObject + + + + + +

+ + + + +
+
+
+
+22
+23
+24
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 22
+
+def self.default_tenant_class
+  @@default_tenant_class ||= nil
+end
+
+
+ +
+

+ + .default_tenant_class=(tenant_class) ⇒ Object + + + + + +

+
+ +

rubocop:disable Style/ClassVars In some cases we only have an ID - if defined we’ll return the default tenant class in such cases

+ + +
+
+
+ + +
+ + + + +
+
+
+
+18
+19
+20
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 18
+
+def self.default_tenant_class=(tenant_class)
+  @@default_tenant_class = tenant_class
+end
+
+
+ +
+

+ + .enable_query_monitorObject + + + + + +

+ + + + +
+
+
+
+10
+11
+12
+
+
# File 'lib/activerecord-multi-tenant/query_monitor.rb', line 10
+
+def self.enable_query_monitor
+  @@enable_query_monitor = true
+end
+
+
+ +
+

+ + .enable_write_only_modeObject + + + + + +

+
+ +

Write-only Mode - this only adds the tenant_id to new records, but doesn’t require its presence for SELECTs/UPDATEs/DELETEs

+ + +
+
+
+ + +
+ + + + +
+
+
+
+28
+29
+30
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 28
+
+def self.enable_write_only_mode
+  @@enable_write_only_mode = true
+end
+
+
+ +
+

+ + .load_current_tenant!Object + + + + + +

+ + + + +
+
+
+
+91
+92
+93
+94
+95
+96
+97
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 91
+
+def self.load_current_tenant!
+  return MultiTenant.current_tenant if MultiTenant.current_tenant && !current_tenant_is_id?
+  raise 'MultiTenant.current_tenant must be set to load' if MultiTenant.current_tenant.nil?
+
+  klass = MultiTenant.default_tenant_class || raise('Only have tenant id, and no default tenant class set')
+  self.current_tenant = klass.find(MultiTenant.current_tenant_id)
+end
+
+
+ +
+

+ + .multi_tenant_model_for_arel(arel) ⇒ Object + + + + + +

+ + + + +
+
+
+
+57
+58
+59
+60
+61
+62
+63
+64
+65
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 57
+
+def self.multi_tenant_model_for_arel(arel)
+  return nil unless arel.respond_to?(:ast)
+
+  if arel.ast.relation.is_a? Arel::Nodes::JoinSource
+    MultiTenant.multi_tenant_model_for_table(arel.ast.relation.left.table_name)
+  else
+    MultiTenant.multi_tenant_model_for_table(arel.ast.relation.table_name)
+  end
+end
+
+
+ +
+

+ + .multi_tenant_model_for_table(table_name) ⇒ Object + + + + + +

+ + + + +
+
+
+
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 44
+
+def self.multi_tenant_model_for_table(table_name)
+  @@multi_tenant_models ||= []
+
+  unless defined?(@@multi_tenant_model_table_names)
+    @@multi_tenant_model_table_names = @@multi_tenant_models.map do |model|
+      [model.table_name, model] if model.table_name
+    end.compact.to_h
+  end
+
+  @@multi_tenant_model_table_names[table_name.to_s]
+  # rubocop:enable Style/ClassVars
+end
+
+
+ +
+

+ + .partition_key(tenant_name) ⇒ Object + + + + + +

+ + + + +
+
+
+
+12
+13
+14
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 12
+
+def self.partition_key(tenant_name)
+  "#{tenant_name}_id"
+end
+
+
+ +
+

+ + .query_monitor_enabled?Boolean + + + + + +

+
+ + +
+
+
+ +

Returns:

+
    + +
  • + + + (Boolean) + + + +
  • + +
+ +
+ + + + +
+
+
+
+14
+15
+16
+
+
# File 'lib/activerecord-multi-tenant/query_monitor.rb', line 14
+
+def self.query_monitor_enabled?
+  @@enable_query_monitor
+end
+
+
+ +
+

+ + .register_multi_tenant_model(model_klass) ⇒ Object + + + + + +

+
+ +

Registry that maps table names to models (used by the query rewriter)

+ + +
+
+
+ + +
+ + + + +
+
+
+
+37
+38
+39
+40
+41
+42
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 37
+
+def self.register_multi_tenant_model(model_klass)
+  @@multi_tenant_models ||= []
+  @@multi_tenant_models.push(model_klass)
+
+  remove_class_variable(:@@multi_tenant_model_table_names) if defined?(@@multi_tenant_model_table_names)
+end
+
+
+ +
+

+ + .tenant_klass_defined?(tenant_name) ⇒ Boolean + + + + + +

+
+ + +
+
+
+ +

Returns:

+
    + +
  • + + + (Boolean) + + + +
  • + +
+ +
+ + + + +
+
+
+
+8
+9
+10
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 8
+
+def self.tenant_klass_defined?(tenant_name)
+  !!tenant_name.to_s.classify.safe_constantize
+end
+
+
+ +
+

+ + .with(tenant, &block) ⇒ Object + + + + + +

+ + + + +
+
+
+
+99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 99
+
+def self.with(tenant, &block)
+  return block.call if current_tenant == tenant
+
+  old_tenant = current_tenant
+  begin
+    self.current_tenant = tenant
+    block.call
+  ensure
+    self.current_tenant = old_tenant
+  end
+end
+
+
+ +
+

+ + .with_write_only_mode_enabled?Boolean + + + + + +

+
+ + +
+
+
+ +

Returns:

+
    + +
  • + + + (Boolean) + + + +
  • + +
+ +
+ + + + +
+
+
+
+32
+33
+34
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 32
+
+def self.with_write_only_mode_enabled?
+  @@enable_write_only_mode ||= false
+end
+
+
+ +
+

+ + .without(&block) ⇒ Object + + + + + +

+ + + + +
+
+
+
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 111
+
+def self.without(&block)
+  return block.call if current_tenant.nil?
+
+  old_tenant = current_tenant
+  begin
+    self.current_tenant = nil
+    block.call
+  ensure
+    self.current_tenant = old_tenant
+  end
+end
+
+
+ +
+

+ + .wrap_methods(klass, owner, *method_names) ⇒ Object + + + + + +

+ + + + +
+
+
+
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 129
+
+def self.wrap_methods(klass, owner, *method_names)
+  method_names.each do |method_name|
+    original_method_name = :"_mt_original_#{method_name}"
+    klass.class_eval <<-CODE, __FILE__, __LINE__ + 1
+      alias_method :#{original_method_name}, :#{method_name}
+      def #{method_name}(*args, &block)
+        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)
+          MultiTenant.with(#{owner}.public_send(#{owner}.class.partition_key)) { #{original_method_name}(*args, &block) }
+        else
+          #{original_method_name}(*args, &block)
+        end
+      end
+    CODE
+  end
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/ArelTenantVisitor.html b/docs/source/_static/api-reference/MultiTenant/ArelTenantVisitor.html new file mode 100644 index 00000000..c27cf8b9 --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/ArelTenantVisitor.html @@ -0,0 +1,755 @@ + + + + + + + Class: MultiTenant::ArelTenantVisitor + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: MultiTenant::ArelTenantVisitor + + + +

+
+ +
+
Inherits:
+
+ Object + +
    +
  • Object
  • + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/query_rewriter.rb
+
+ +
+ + + + + +

Instance Attribute Summary collapse

+
    + +
  • + + + #contexts ⇒ Object + + + + + + + + + readonly + + + + + + + + + +
    +

    Returns the value of attribute contexts.

    +
    + +
  • + + +
+ + + + + +

+ Instance Method Summary + collapse +

+ + + + +
+

Constructor Details

+ +
+

+ + #initialize(arel) ⇒ ArelTenantVisitor + + + + + +

+
+ +

Returns a new instance of ArelTenantVisitor.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+66
+67
+68
+69
+70
+71
+72
+73
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 66
+
+def initialize(arel)
+  super(proc {})
+  @statement_node_id = nil
+
+  @contexts = []
+  @current_context = nil
+  accept(arel.ast)
+end
+
+
+ +
+ +
+

Instance Attribute Details

+ + + +
+

+ + #contextsObject (readonly) + + + + + +

+
+ +

Returns the value of attribute contexts.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+75
+76
+77
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 75
+
+def contexts
+  @contexts
+end
+
+
+ +
+ + +
+

Instance Method Details

+ + +
+

+ + #visit_Arel_Attributes_Attribute(*args) ⇒ Object + + + + + +

+
+ +

rubocop:disable Naming/MethodName

+ + +
+
+
+ + +
+ + + + +
+
+
+
+78
+79
+80
+81
+82
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 78
+
+def visit_Arel_Attributes_Attribute(*args)
+  return if @current_context.nil?
+
+  super(*args)
+end
+
+
+ +
+

+ + #visit_Arel_Nodes_Equality(obj, *args) ⇒ Object + + + + + +

+ + + + +
+
+
+
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 84
+
+def visit_Arel_Nodes_Equality(obj, *args)
+  if obj.left.is_a?(Arel::Attributes::Attribute)
+    table_name = obj.left.relation.table_name
+    model = MultiTenant.multi_tenant_model_for_table(table_name)
+    if model.present? && obj.left.name.to_s == model.partition_key.to_s
+      @current_context.visited_handled_relation(obj.left.relation)
+    end
+  end
+  super(obj, *args)
+end
+
+
+ +
+

+ + #visit_Arel_Nodes_OuterJoin(obj, _collector = nil) ⇒ Object + + + + Also known as: + visit_Arel_Nodes_FullOuterJoin, visit_Arel_Nodes_RightOuterJoin + + + + +

+
+ +

rubocop:disable Naming/MethodName

+ + +
+
+
+ + +
+ + + + +
+
+
+
+128
+129
+130
+131
+132
+133
+134
+135
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 128
+
+def visit_Arel_Nodes_OuterJoin(obj, _collector = nil)
+  nest_context(obj) do
+    @current_context.discover_relations do
+      visit obj.left
+      visit obj.right
+    end
+  end
+end
+
+
+ +
+

+ + #visit_Arel_Nodes_SelectCore(obj, *_args) ⇒ Object + + + + + +

+ + + + +
+
+
+
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 109
+
+def visit_Arel_Nodes_SelectCore(obj, *_args)
+  nest_context(obj) do
+    @current_context.discover_relations do
+      visit obj.source
+    end
+    visit obj.wheres
+    visit obj.groups
+    visit obj.windows
+    if defined?(obj.having)
+      visit obj.having
+    else
+      visit obj.havings
+    end
+  end
+end
+
+
+ +
+

+ + #visit_Arel_Table(obj, _collector = nil) ⇒ Object + + + + Also known as: + visit_Arel_Nodes_TableAlias + + + + +

+ + + + +
+
+
+
+103
+104
+105
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 103
+
+def visit_Arel_Table(obj, _collector = nil)
+  @current_context.visited_relation(obj) if tenant_relation?(obj.table_name)
+end
+
+
+ +
+

+ + #visit_MultiTenant_TenantEnforcementClause(obj) ⇒ Object + + + + + +

+ + + + +
+
+
+
+95
+96
+97
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 95
+
+def visit_MultiTenant_TenantEnforcementClause(obj, *)
+  @current_context.visited_handled_relation(obj.tenant_attribute.relation)
+end
+
+
+ +
+

+ + #visit_MultiTenant_TenantJoinEnforcementClause(obj) ⇒ Object + + + + + +

+ + + + +
+
+
+
+99
+100
+101
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 99
+
+def visit_MultiTenant_TenantJoinEnforcementClause(obj, *)
+  @current_context.visited_handled_relation(obj.tenant_attribute.relation)
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/ArelVisitorsDepthFirst.html b/docs/source/_static/api-reference/MultiTenant/ArelVisitorsDepthFirst.html new file mode 100644 index 00000000..2bb4c0f6 --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/ArelVisitorsDepthFirst.html @@ -0,0 +1,208 @@ + + + + + + + Class: MultiTenant::ArelVisitorsDepthFirst + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: MultiTenant::ArelVisitorsDepthFirst + + + +

+
+ +
+
Inherits:
+
+ Arel::Visitors::Visitor + +
    +
  • Object
  • + + + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/arel_visitors_depth_first.rb
+
+ +
+ + + + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + +
+

Constructor Details

+ +
+

+ + #initialize(block = nil) ⇒ ArelVisitorsDepthFirst + + + + + +

+
+ +

Returns a new instance of ArelVisitorsDepthFirst.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+5
+6
+7
+8
+
+
# File 'lib/activerecord-multi-tenant/arel_visitors_depth_first.rb', line 5
+
+def initialize(block = nil)
+  @block = block || proc
+  super()
+end
+
+
+ +
+ + +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/BaseTenantEnforcementClause.html b/docs/source/_static/api-reference/MultiTenant/BaseTenantEnforcementClause.html new file mode 100644 index 00000000..e8f67a0a --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/BaseTenantEnforcementClause.html @@ -0,0 +1,462 @@ + + + + + + + Class: MultiTenant::BaseTenantEnforcementClause + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: MultiTenant::BaseTenantEnforcementClause + + + +

+
+ +
+
Inherits:
+
+ Arel::Nodes::Node + +
    +
  • Object
  • + + + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/query_rewriter.rb
+
+ +
+ + + + + + +

Instance Attribute Summary collapse

+
    + +
  • + + + #tenant_attribute ⇒ Object + + + + + + + + + readonly + + + + + + + + + +
    +

    Returns the value of attribute tenant_attribute.

    +
    + +
  • + + +
+ + + + + +

+ Instance Method Summary + collapse +

+ + + + + +
+

Constructor Details

+ +
+

+ + #initialize(tenant_attribute) ⇒ BaseTenantEnforcementClause + + + + + +

+
+ +

Returns a new instance of BaseTenantEnforcementClause.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+179
+180
+181
+182
+183
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 179
+
+def initialize(tenant_attribute)
+  super()
+  @tenant_attribute = tenant_attribute
+  @tenant_model = MultiTenant.multi_tenant_model_for_table(tenant_attribute.relation.table_name)
+end
+
+
+ +
+ +
+

Instance Attribute Details

+ + + +
+

+ + #tenant_attributeObject (readonly) + + + + + +

+
+ +

Returns the value of attribute tenant_attribute.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+177
+178
+179
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 177
+
+def tenant_attribute
+  @tenant_attribute
+end
+
+
+ +
+ + +
+

Instance Method Details

+ + +
+

+ + #to_sObject + + + + + +

+ + + + +
+
+
+
+185
+186
+187
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 185
+
+def to_s
+  to_sql
+end
+
+
+ +
+

+ + #to_sqlObject + + + + + +

+ + + + +
+
+
+
+193
+194
+195
+196
+197
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 193
+
+def to_sql(*)
+  collector = Arel::Collectors::SQLString.new
+  collector = @tenant_model.connection.visitor.accept tenant_arel, collector
+  collector.value
+end
+
+
+ +
+

+ + #to_strObject + + + + + +

+ + + + +
+
+
+
+189
+190
+191
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 189
+
+def to_str
+  to_sql
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/Context.html b/docs/source/_static/api-reference/MultiTenant/Context.html new file mode 100644 index 00000000..2d1fa6dc --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/Context.html @@ -0,0 +1,659 @@ + + + + + + + Class: MultiTenant::Context + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: MultiTenant::Context + + + +

+
+ +
+
Inherits:
+
+ Object + +
    +
  • Object
  • + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/query_rewriter.rb
+
+ +
+ + + + + +

Instance Attribute Summary collapse

+
    + +
  • + + + #arel_node ⇒ Object + + + + + + + + + readonly + + + + + + + + + +
    +

    Returns the value of attribute arel_node.

    +
    + +
  • + + +
  • + + + #handled_relations ⇒ Object + + + + + + + + + readonly + + + + + + + + + +
    +

    Returns the value of attribute handled_relations.

    +
    + +
  • + + +
  • + + + #known_relations ⇒ Object + + + + + + + + + readonly + + + + + + + + + +
    +

    Returns the value of attribute known_relations.

    +
    + +
  • + + +
+ + + + + +

+ Instance Method Summary + collapse +

+ + + + +
+

Constructor Details

+ +
+

+ + #initialize(arel_node) ⇒ Context + + + + + +

+
+ +

Returns a new instance of Context.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+32
+33
+34
+35
+36
+37
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 32
+
+def initialize(arel_node)
+  @arel_node = arel_node
+  @known_relations = []
+  @handled_relations = []
+  @discovering = false
+end
+
+
+ +
+ +
+

Instance Attribute Details

+ + + +
+

+ + #arel_nodeObject (readonly) + + + + + +

+
+ +

Returns the value of attribute arel_node.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+30
+31
+32
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 30
+
+def arel_node
+  @arel_node
+end
+
+
+ + + +
+

+ + #handled_relationsObject (readonly) + + + + + +

+
+ +

Returns the value of attribute handled_relations.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+30
+31
+32
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 30
+
+def handled_relations
+  @handled_relations
+end
+
+
+ + + +
+

+ + #known_relationsObject (readonly) + + + + + +

+
+ +

Returns the value of attribute known_relations.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+30
+31
+32
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 30
+
+def known_relations
+  @known_relations
+end
+
+
+ +
+ + +
+

Instance Method Details

+ + +
+

+ + #discover_relationsObject + + + + + +

+ + + + +
+
+
+
+39
+40
+41
+42
+43
+44
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 39
+
+def discover_relations
+  old_discovering = @discovering
+  @discovering = true
+  yield
+  @discovering = old_discovering
+end
+
+
+ +
+

+ + #unhandled_relationsObject + + + + + +

+ + + + +
+
+
+
+56
+57
+58
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 56
+
+def unhandled_relations
+  known_relations.uniq - handled_relations
+end
+
+
+ +
+

+ + #visited_handled_relation(relation) ⇒ Object + + + + + +

+ + + + +
+
+
+
+52
+53
+54
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 52
+
+def visited_handled_relation(relation)
+  @handled_relations << Table.new(relation)
+end
+
+
+ +
+

+ + #visited_relation(relation) ⇒ Object + + + + + +

+ + + + +
+
+
+
+46
+47
+48
+49
+50
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 46
+
+def visited_relation(relation)
+  return unless @discovering
+
+  @known_relations << Table.new(relation)
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/ControllerExtensions.html b/docs/source/_static/api-reference/MultiTenant/ControllerExtensions.html new file mode 100644 index 00000000..20b69bc3 --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/ControllerExtensions.html @@ -0,0 +1,202 @@ + + + + + + + Module: MultiTenant::ControllerExtensions + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: MultiTenant::ControllerExtensions + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/controller_extensions.rb
+
+ +
+ + + + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Instance Method Details

+ + +
+

+ + #set_current_tenant_through_filterObject + + + + + +

+ + + + +
+
+
+
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+
+
# File 'lib/activerecord-multi-tenant/controller_extensions.rb', line 9
+
+def set_current_tenant_through_filter
+  class_eval do
+    helper_method :current_tenant if respond_to?(:helper_method)
+
+    private
+
+    # rubocop:disable Naming/AccessorMethodName
+    def set_current_tenant(current_tenant_object)
+      MultiTenant.current_tenant = current_tenant_object
+    end
+    # rubocop:enable Naming/AccessorMethodName
+
+    def current_tenant
+      MultiTenant.current_tenant
+    end
+  end
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/CopyFromClient.html b/docs/source/_static/api-reference/MultiTenant/CopyFromClient.html new file mode 100644 index 00000000..de6a7790 --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/CopyFromClient.html @@ -0,0 +1,186 @@ + + + + + + + Module: MultiTenant::CopyFromClient + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: MultiTenant::CopyFromClient + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/copy_from_client.rb
+
+ +
+ + + + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Instance Method Details

+ + +
+

+ + #copy_from_client(columns, &block) ⇒ Object + + + + + +

+ + + + +
+
+
+
+22
+23
+24
+25
+26
+27
+28
+29
+30
+
+
# File 'lib/activerecord-multi-tenant/copy_from_client.rb', line 22
+
+def copy_from_client(columns, &block)
+  conn         = connection.raw_connection
+  column_types = columns.map { |c| type_for_attribute(c.to_s) }
+  helper = MultiTenant::CopyFromClientHelper.new(conn, column_types)
+  conn.copy_data %{COPY #{quoted_table_name}("#{columns.join('","')}") FROM STDIN}, PG::TextEncoder::CopyRow.new do
+    block.call helper
+  end
+  helper.count
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/CopyFromClientHelper.html b/docs/source/_static/api-reference/MultiTenant/CopyFromClientHelper.html new file mode 100644 index 00000000..25028d7e --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/CopyFromClientHelper.html @@ -0,0 +1,362 @@ + + + + + + + Class: MultiTenant::CopyFromClientHelper + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: MultiTenant::CopyFromClientHelper + + + +

+
+ +
+
Inherits:
+
+ Object + +
    +
  • Object
  • + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/copy_from_client.rb
+
+ +
+ +

Overview

+
+ +

Designed to be mixed into an ActiveRecord model to provide a copy_from_client method that allows for efficient bulk insertion of data into a PostgreSQL database using the COPY command

+ + +
+
+
+ + +
+ + + +

Instance Attribute Summary collapse

+
    + +
  • + + + #count ⇒ Object + + + + + + + + + readonly + + + + + + + + + +
    +

    Returns the value of attribute count.

    +
    + +
  • + + +
+ + + + + +

+ Instance Method Summary + collapse +

+ + + + +
+

Constructor Details

+ +
+

+ + #initialize(conn, column_types) ⇒ CopyFromClientHelper + + + + + +

+
+ +

Returns a new instance of CopyFromClientHelper.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+8
+9
+10
+11
+12
+
+
# File 'lib/activerecord-multi-tenant/copy_from_client.rb', line 8
+
+def initialize(conn, column_types)
+  @count = 0
+  @conn = conn
+  @column_types = column_types
+end
+
+
+ +
+ +
+

Instance Attribute Details

+ + + +
+

+ + #countObject (readonly) + + + + + +

+
+ +

Returns the value of attribute count.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+6
+7
+8
+
+
# File 'lib/activerecord-multi-tenant/copy_from_client.rb', line 6
+
+def count
+  @count
+end
+
+
+ +
+ + +
+

Instance Method Details

+ + +
+

+ + #<<(row) ⇒ Object + + + + + +

+ + + + +
+
+
+
+14
+15
+16
+17
+18
+
+
# File 'lib/activerecord-multi-tenant/copy_from_client.rb', line 14
+
+def <<(row)
+  row = row.map.with_index { |val, idx| @column_types[idx].serialize(val) }
+  @conn.put_copy_data(row)
+  @count += 1
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/Current.html b/docs/source/_static/api-reference/MultiTenant/Current.html new file mode 100644 index 00000000..c88b2fed --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/Current.html @@ -0,0 +1,124 @@ + + + + + + + Class: MultiTenant::Current + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: MultiTenant::Current + + + +

+
+ +
+
Inherits:
+
+ ActiveSupport::CurrentAttributes + +
    +
  • Object
  • + + + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/multi_tenant.rb
+
+ +
+ + + + + + + + + + + +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/DatabaseStatements.html b/docs/source/_static/api-reference/MultiTenant/DatabaseStatements.html new file mode 100644 index 00000000..4cedf31d --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/DatabaseStatements.html @@ -0,0 +1,366 @@ + + + + + + + Module: MultiTenant::DatabaseStatements + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: MultiTenant::DatabaseStatements + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/query_rewriter.rb
+
+ +
+ + + + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Instance Method Details

+ + +
+

+ + #delete(arel, name = nil, binds = []) ⇒ Object + + + + + +

+ + + + +
+
+
+
+268
+269
+270
+271
+272
+273
+274
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 268
+
+def delete(arel, name = nil, binds = [])
+  model = MultiTenant.multi_tenant_model_for_arel(arel)
+  if model.present? && !MultiTenant.with_write_only_mode_enabled? && MultiTenant.current_tenant_id.present?
+    arel.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key]))
+  end
+  super(arel, name, binds)
+end
+
+
+ +
+

+ + #join_to_delete(delete, *args) ⇒ Object + + + + + +

+ + + + +
+
+
+
+251
+252
+253
+254
+255
+256
+257
+258
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 251
+
+def join_to_delete(delete, *args)
+  delete = super(delete, *args)
+  model = MultiTenant.multi_tenant_model_for_table(delete.ast.left.table_name)
+  if model.present? && !MultiTenant.with_write_only_mode_enabled? && MultiTenant.current_tenant_id.present?
+    delete.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key]))
+  end
+  delete
+end
+
+
+ +
+

+ + #join_to_update(update, *args) ⇒ Object + + + + + +

+ + + + +
+
+
+
+242
+243
+244
+245
+246
+247
+248
+249
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 242
+
+def join_to_update(update, *args)
+  update = super(update, *args)
+  model = MultiTenant.multi_tenant_model_for_table(update.ast.relation.table_name)
+  if model.present? && !MultiTenant.with_write_only_mode_enabled? && MultiTenant.current_tenant_id.present?
+    update.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key]))
+  end
+  update
+end
+
+
+ +
+

+ + #update(arel, name = nil, binds = []) ⇒ Object + + + + + +

+ + + + +
+
+
+
+260
+261
+262
+263
+264
+265
+266
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 260
+
+def update(arel, name = nil, binds = [])
+  model = MultiTenant.multi_tenant_model_for_arel(arel)
+  if model.present? && !MultiTenant.with_write_only_mode_enabled? && MultiTenant.current_tenant_id.present?
+    arel.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key]))
+  end
+  super(arel, name, binds)
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/FastTruncate.html b/docs/source/_static/api-reference/MultiTenant/FastTruncate.html new file mode 100644 index 00000000..d799e294 --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/FastTruncate.html @@ -0,0 +1,226 @@ + + + + + + + Module: MultiTenant::FastTruncate + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: MultiTenant::FastTruncate + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/fast_truncate.rb
+
+ +
+ + + + + + + + + +

+ Class Method Summary + collapse +

+ + + + + + +
+

Class Method Details

+ + +
+

+ + .run(exclude: ['schema_migrations']) ⇒ Object + + + + + +

+ + + + +
+
+
+
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+
+
# File 'lib/activerecord-multi-tenant/fast_truncate.rb', line 6
+
+def self.run(exclude: ['schema_migrations'])
+  # This is a slightly faster version of DatabaseCleaner.clean_with(:truncation, pre_count: true)
+  ActiveRecord::Base.connection.execute format(%(
+  DO LANGUAGE plpgsql $$
+  DECLARE
+    t record;
+    tables text[];
+    seq_exists boolean;
+    needs_truncate boolean;
+  BEGIN
+    FOR t IN SELECT schemaname, tablename FROM pg_tables WHERE schemaname = 'public' AND tablename NOT IN (%s) LOOP
+      EXECUTE 'SELECT EXISTS (SELECT * from pg_class c WHERE c.relkind = ''S''
+       AND c.relname=''' || t.tablename || '_id_seq'')' into seq_exists;
+      IF seq_exists THEN
+        EXECUTE 'SELECT is_called FROM ' || t.tablename || '_id_seq' INTO needs_truncate;
+      ELSE
+        needs_truncate := true;
+      END IF;
+
+      IF needs_truncate THEN
+        tables := array_append(tables, quote_ident(t.schemaname) || '.' || quote_ident(t.tablename));
+      END IF;
+    END LOOP;
+
+    IF array_length(tables, 1) > 0 THEN
+      EXECUTE 'TRUNCATE TABLE ' || array_to_string(tables, ', ') || ' RESTART IDENTITY CASCADE';
+    END IF;
+  END$$;), exclude.map { |t| "'#{t}'" }.join('\n'))
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/MigrationExtensions.html b/docs/source/_static/api-reference/MultiTenant/MigrationExtensions.html new file mode 100644 index 00000000..b4292500 --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/MigrationExtensions.html @@ -0,0 +1,554 @@ + + + + + + + Module: MultiTenant::MigrationExtensions + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: MultiTenant::MigrationExtensions + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/migrations.rb
+
+ +
+ + + + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Instance Method Details

+ + +
+

+ + #citus_versionObject + + + + + +

+ + + + +
+
+
+
+59
+60
+61
+62
+63
+
+
# File 'lib/activerecord-multi-tenant/migrations.rb', line 59
+
+def citus_version
+  execute("SELECT extversion FROM pg_extension WHERE extname = 'citus'").getvalue(0, 0).try(:split, '-').try(:first)
+rescue ArgumentError => e
+  raise unless e.message == 'invalid tuple number 0'
+end
+
+
+ +
+

+ + #create_distributed_table(table_name, partition_key) ⇒ Object + + + + + +

+ + + + +
+
+
+
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+
+
# File 'lib/activerecord-multi-tenant/migrations.rb', line 3
+
+def create_distributed_table(table_name, partition_key)
+  return unless citus_version.present?
+
+  reversible do |dir|
+    dir.up do
+      execute "SELECT create_distributed_table($$#{table_name}$$, $$#{partition_key}$$)"
+    end
+    dir.down do
+      undistribute_table(table_name)
+    end
+  end
+end
+
+
+ +
+

+ + #create_reference_table(table_name) ⇒ Object + + + + + +

+ + + + +
+
+
+
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+
+
# File 'lib/activerecord-multi-tenant/migrations.rb', line 16
+
+def create_reference_table(table_name)
+  return unless citus_version.present?
+
+  reversible do |dir|
+    dir.up do
+      execute "SELECT create_reference_table($$#{table_name}$$)"
+    end
+    dir.down do
+      undistribute_table(table_name)
+    end
+  end
+end
+
+
+ +
+

+ + #enable_extension_on_all_nodes(extension) ⇒ Object + + + + + +

+ + + + +
+
+
+
+55
+56
+57
+
+
# File 'lib/activerecord-multi-tenant/migrations.rb', line 55
+
+def enable_extension_on_all_nodes(extension)
+  execute_on_all_nodes "CREATE EXTENSION IF NOT EXISTS \"#{extension}\""
+end
+
+
+ +
+

+ + #execute_on_all_nodes(sql) ⇒ Object + + + + + +

+ + + + +
+
+
+
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+
+
# File 'lib/activerecord-multi-tenant/migrations.rb', line 41
+
+def execute_on_all_nodes(sql)
+  execute sql
+
+  case citus_version
+  when '6.0'
+    execute "SELECT citus_run_on_all_workers($$#{sql}$$)" # initial citus_tools.sql with different names
+  when nil
+    # Do nothing, this is regular Postgres
+  else
+    # 6.1 and newer
+    execute "SELECT run_command_on_workers($$#{sql}$$)"
+  end
+end
+
+
+ +
+

+ + #rebalance_table_shardsObject + + + + + +

+ + + + +
+
+
+
+35
+36
+37
+38
+39
+
+
# File 'lib/activerecord-multi-tenant/migrations.rb', line 35
+
+def rebalance_table_shards
+  return unless citus_version.present?
+
+  execute 'SELECT rebalance_table_shards()'
+end
+
+
+ +
+

+ + #undistribute_table(table_name) ⇒ Object + + + + + +

+ + + + +
+
+
+
+29
+30
+31
+32
+33
+
+
# File 'lib/activerecord-multi-tenant/migrations.rb', line 29
+
+def undistribute_table(table_name)
+  return unless citus_version.present?
+
+  execute "SELECT undistribute_table($$#{table_name}$$))"
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/MissingTenantError.html b/docs/source/_static/api-reference/MultiTenant/MissingTenantError.html new file mode 100644 index 00000000..cff4c2c9 --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/MissingTenantError.html @@ -0,0 +1,124 @@ + + + + + + + Exception: MultiTenant::MissingTenantError + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Exception: MultiTenant::MissingTenantError + + + +

+
+ +
+
Inherits:
+
+ StandardError + +
    +
  • Object
  • + + + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/multi_tenant.rb
+
+ +
+ + + + + + + + + + + +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/ModelExtensionsClassMethods.html b/docs/source/_static/api-reference/MultiTenant/ModelExtensionsClassMethods.html new file mode 100644 index 00000000..ccc822f6 --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/ModelExtensionsClassMethods.html @@ -0,0 +1,492 @@ + + + + + + + Module: MultiTenant::ModelExtensionsClassMethods + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: MultiTenant::ModelExtensionsClassMethods + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/model_extensions.rb
+
+ +
+ +

Overview

+
+ +

Extension to the model to allow scoping of models to the current tenant. This is done by adding the multitenant method to the models that need to be scoped. This method is called in the model declaration. Adds scoped_by_tenant? partition_key, primary_key and inherited methods to the model

+ + +
+
+
+ + +
+ +

+ Constant Summary + collapse +

+ +
+ +
DEFAULT_ID_FIELD = + +
+
'id'.freeze
+ +
+ + + + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Instance Method Details

+ + +
+

+ + #multi_tenant(tenant_name, options = {}) ⇒ Object + + + + + +

+
+ +

executes when multi_tenant method is called in the model. This method adds the following methods to the model that calls it. scoped_by_tenant? - returns true if the model is scoped by tenant partition_key - returns the partition key for the model primary_key - returns the primary key for the model

+ + +
+
+
+ + +
+ + + + +
+
+
+
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+94
+95
+96
+97
+98
+99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+
+
# File 'lib/activerecord-multi-tenant/model_extensions.rb', line 16
+
+def multi_tenant(tenant_name, options = {})
+  if to_s.underscore.to_sym == tenant_name || (!table_name.nil? && table_name.singularize.to_sym == tenant_name)
+    unless MultiTenant.with_write_only_mode_enabled?
+      # This is the tenant model itself. Workaround for https://github.com/citusdata/citus/issues/687
+      before_create lambda {
+        id = if self.class.columns_hash[self.class.primary_key].type == :uuid
+               SecureRandom.uuid
+             else
+               self.class.connection.select_value(
+                 "SELECT nextval('#{self.class.table_name}_#{self.class.primary_key}_seq'::regclass)"
+               )
+             end
+        self.id ||= id
+      }
+    end
+  else
+    class << self
+      def scoped_by_tenant?
+        true
+      end
+
+      # Allow partition_key to be set from a superclass if not already set in this class
+      def partition_key
+        @partition_key ||= ancestors.detect { |k| k.instance_variable_get(:@partition_key) }
+                                    .try(:instance_variable_get, :@partition_key)
+      end
+
+      # Avoid primary_key errors when using composite primary keys (e.g. id, tenant_id)
+      def primary_key
+        if defined?(PRIMARY_KEY_NOT_SET) ? !PRIMARY_KEY_NOT_SET.equal?(@primary_key) : @primary_key
+          return @primary_key
+        end
+
+        primary_object_keys = Array.wrap(connection.schema_cache.primary_keys(table_name)) - [partition_key]
+
+        @primary_key = if primary_object_keys.size == 1
+                         primary_object_keys.first
+                       elsif connection.schema_cache.columns_hash(table_name).include? DEFAULT_ID_FIELD
+                         DEFAULT_ID_FIELD
+                       end
+      end
+
+      def inherited(subclass)
+        super
+        MultiTenant.register_multi_tenant_model(subclass)
+      end
+    end
+
+    MultiTenant.register_multi_tenant_model(self)
+
+    @partition_key = options[:partition_key] || MultiTenant.partition_key(tenant_name)
+    partition_key = @partition_key
+
+    # Create an implicit belongs_to association only if tenant class exists
+    if MultiTenant.tenant_klass_defined?(tenant_name)
+      belongs_to tenant_name, **options.slice(:class_name, :inverse_of, :optional)
+                                       .merge(foreign_key: options[:partition_key])
+    end
+
+    # New instances should have the tenant set
+    after_initialize proc { |record|
+      if MultiTenant.current_tenant_id &&
+         (!record.attribute_present?(partition_key) || record.public_send(partition_key.to_sym).nil?)
+        record.public_send("#{partition_key}=".to_sym, MultiTenant.current_tenant_id)
+      end
+    }
+
+    # Below block adds the following methods to the model that calls it.
+    # partition_key= - returns the partition key for the model.class << self 'partition' method defined above
+    # is the getter method. Here, there is additional check to assure that the tenant id is not changed once set
+    # tenant_name- returns the name of the tenant model. Its setter and getter methods defined separately
+    # Getter checks for the tenant association and if it is not loaded, returns the current tenant id set
+    # in the MultiTenant module
+    to_include = Module.new do
+      define_method "#{partition_key}=" do |tenant_id|
+        write_attribute(partition_key.to_s, tenant_id)
+
+        # Rails 5 `attribute_will_change!` uses the attribute-method-call rather than `read_attribute`
+        # and will raise ActiveModel::MissingAttributeError if that column was not selected.
+        # This is rescued as NoMethodError and in MRI attribute_was is assigned an arbitrary Object
+        was = send("#{partition_key}_was")
+        was_nil_or_skipped = was.nil? || was.instance_of?(Object)
+
+        if send("#{partition_key}_changed?") && persisted? && !was_nil_or_skipped
+          raise MultiTenant::TenantIsImmutable
+        end
+
+        tenant_id
+      end
+
+      if MultiTenant.tenant_klass_defined?(tenant_name)
+        define_method "#{tenant_name}=" do |model|
+          super(model)
+          if send("#{partition_key}_changed?") && persisted? && !send("#{partition_key}_was").nil?
+            raise MultiTenant::TenantIsImmutable
+          end
+
+          model
+        end
+
+        define_method tenant_name.to_s do
+          if !association(tenant_name.to_sym).loaded? && !MultiTenant.current_tenant_is_id? &&
+             MultiTenant.current_tenant_id && public_send(partition_key) == MultiTenant.current_tenant_id
+            MultiTenant.current_tenant
+          else
+            super()
+          end
+        end
+      end
+    end
+    include to_include
+
+    # Below blocks sets tenant_id for the current session with the tenant_id of the record
+    # If the tenant is not set for the `session.After` the save operation current session tenant is set to nil
+    # If tenant is set for the session, save operation is performed as it is
+    around_save lambda { |record, block|
+      record_tenant = record.attribute_was(partition_key)
+      if persisted? && MultiTenant.current_tenant_id.nil? && !record_tenant.nil?
+        MultiTenant.with(record.public_send(partition_key)) { block.call }
+      else
+        block.call
+      end
+    }
+
+    around_update lambda { |record, block|
+      record_tenant = record.attribute_was(partition_key)
+      if MultiTenant.current_tenant_id.nil? && !record_tenant.nil?
+        MultiTenant.with(record.public_send(partition_key)) { block.call }
+      else
+        block.call
+      end
+    }
+
+    around_destroy lambda { |record, block|
+      if MultiTenant.current_tenant_id.nil?
+        MultiTenant.with(record.public_send(partition_key)) { block.call }
+      else
+        block.call
+      end
+    }
+  end
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/QueryMonitor.html b/docs/source/_static/api-reference/MultiTenant/QueryMonitor.html new file mode 100644 index 00000000..3828b15b --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/QueryMonitor.html @@ -0,0 +1,257 @@ + + + + + + + Class: MultiTenant::QueryMonitor + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: MultiTenant::QueryMonitor + + + +

+
+ +
+
Inherits:
+
+ Object + +
    +
  • Object
  • + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/query_monitor.rb
+
+ +
+ +

Overview

+
+ +

rubocop:enable Style/ClassVars QueryMonitor class to log a warning when a query fails and there is no tenant set start and finish methods are required to be register sql.active_record hook

+ + +
+
+
+ + +
+ + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Instance Method Details

+ + +
+

+ + #finish(_name, _id, payload) ⇒ Object + + + + + +

+ + + + +
+
+
+
+24
+25
+26
+27
+28
+29
+30
+
+
# File 'lib/activerecord-multi-tenant/query_monitor.rb', line 24
+
+def finish(_name, _id, payload)
+  return unless MultiTenant.query_monitor_enabled?
+
+  return unless payload[:exception].present? && MultiTenant.current_tenant_id.nil?
+
+  Rails.logger.info 'WARNING: Tenant not present - make sure to add MultiTenant.with(tenant) { ... }'
+end
+
+
+ +
+

+ + #start(_name, _id, _payload) ⇒ Object + + + + + +

+ + + + +
+
+
+
+22
+
+
# File 'lib/activerecord-multi-tenant/query_monitor.rb', line 22
+
+def start(_name, _id, _payload) end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/Table.html b/docs/source/_static/api-reference/MultiTenant/Table.html new file mode 100644 index 00000000..bb5c3688 --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/Table.html @@ -0,0 +1,419 @@ + + + + + + + Class: MultiTenant::Table + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: MultiTenant::Table + + + +

+
+ +
+
Inherits:
+
+ Object + +
    +
  • Object
  • + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/query_rewriter.rb
+
+ +
+ + + + + +

Instance Attribute Summary collapse

+
    + +
  • + + + #arel_table ⇒ Object + + + + + + + + + readonly + + + + + + + + + +
    +

    Returns the value of attribute arel_table.

    +
    + +
  • + + +
+ + + + + +

+ Instance Method Summary + collapse +

+ + + + +
+

Constructor Details

+ +
+

+ + #initialize(arel_table) ⇒ Table + + + + + +

+
+ +

Returns a new instance of Table.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+9
+10
+11
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 9
+
+def initialize(arel_table)
+  @arel_table = arel_table
+end
+
+
+ +
+ +
+

Instance Attribute Details

+ + + +
+

+ + #arel_tableObject (readonly) + + + + + +

+
+ +

Returns the value of attribute arel_table.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+7
+8
+9
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 7
+
+def arel_table
+  @arel_table
+end
+
+
+ +
+ + +
+

Instance Method Details

+ + +
+

+ + #eql?(other) ⇒ Boolean + + + + + +

+
+ + +
+
+
+ +

Returns:

+
    + +
  • + + + (Boolean) + + + +
  • + +
+ +
+ + + + +
+
+
+
+13
+14
+15
+16
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 13
+
+def eql?(other)
+  self.class == other.class &&
+    equality_fields.eql?(other.equality_fields)
+end
+
+
+ +
+

+ + #hashObject + + + + + +

+ + + + +
+
+
+
+18
+19
+20
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 18
+
+def hash
+  equality_fields.hash
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/TenantEnforcementClause.html b/docs/source/_static/api-reference/MultiTenant/TenantEnforcementClause.html new file mode 100644 index 00000000..63815f2a --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/TenantEnforcementClause.html @@ -0,0 +1,148 @@ + + + + + + + Class: MultiTenant::TenantEnforcementClause + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: MultiTenant::TenantEnforcementClause + + + +

+
+ +
+
Inherits:
+
+ BaseTenantEnforcementClause + + + show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/query_rewriter.rb
+
+ +
+ + + + + + + +

Instance Attribute Summary

+ +

Attributes inherited from BaseTenantEnforcementClause

+

#tenant_attribute

+ + + + + + + + + +

Method Summary

+ +

Methods inherited from BaseTenantEnforcementClause

+

#initialize, #to_s, #to_sql, #to_str

+ +
+

Constructor Details

+ +

This class inherits a constructor from MultiTenant::BaseTenantEnforcementClause

+ +
+ + +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/TenantIsImmutable.html b/docs/source/_static/api-reference/MultiTenant/TenantIsImmutable.html new file mode 100644 index 00000000..61390538 --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/TenantIsImmutable.html @@ -0,0 +1,135 @@ + + + + + + + Exception: MultiTenant::TenantIsImmutable + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Exception: MultiTenant::TenantIsImmutable + + + +

+
+ +
+
Inherits:
+
+ StandardError + +
    +
  • Object
  • + + + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/multi_tenant.rb
+
+ +
+ +

Overview

+
+ +

This exception is raised when a there is an attempt to change tenant

+ + +
+
+
+ + +
+ + + + + + + + + +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/TenantJoinEnforcementClause.html b/docs/source/_static/api-reference/MultiTenant/TenantJoinEnforcementClause.html new file mode 100644 index 00000000..d16b3457 --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/TenantJoinEnforcementClause.html @@ -0,0 +1,310 @@ + + + + + + + Class: MultiTenant::TenantJoinEnforcementClause + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: MultiTenant::TenantJoinEnforcementClause + + + +

+
+ +
+
Inherits:
+
+ BaseTenantEnforcementClause + + + show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/query_rewriter.rb
+
+ +
+ + + + + +

Instance Attribute Summary collapse

+
    + +
  • + + + #table_left ⇒ Object + + + + + + + + + readonly + + + + + + + + + +
    +

    Returns the value of attribute table_left.

    +
    + +
  • + + +
+ + + + + +

Attributes inherited from BaseTenantEnforcementClause

+

#tenant_attribute

+ + + +

+ Instance Method Summary + collapse +

+ + + + + + + + + + + + + +

Methods inherited from BaseTenantEnforcementClause

+

#to_s, #to_sql, #to_str

+ +
+

Constructor Details

+ +
+

+ + #initialize(tenant_attribute, table_left) ⇒ TenantJoinEnforcementClause + + + + + +

+
+ +

Returns a new instance of TenantJoinEnforcementClause.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+215
+216
+217
+218
+219
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 215
+
+def initialize(tenant_attribute, table_left)
+  super(tenant_attribute)
+  @table_left = table_left
+  @model_left = MultiTenant.multi_tenant_model_for_table(table_left.table_name)
+end
+
+
+ +
+ +
+

Instance Attribute Details

+ + + +
+

+ + #table_leftObject (readonly) + + + + + +

+
+ +

Returns the value of attribute table_left.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+213
+214
+215
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 213
+
+def table_left
+  @table_left
+end
+
+
+ +
+ + +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/TenantValueVisitor.html b/docs/source/_static/api-reference/MultiTenant/TenantValueVisitor.html new file mode 100644 index 00000000..53cd23d9 --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/TenantValueVisitor.html @@ -0,0 +1,239 @@ + + + + + + + Module: MultiTenant::TenantValueVisitor + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: MultiTenant::TenantValueVisitor + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/query_rewriter.rb
+
+ +
+ + + + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Instance Method Details

+ + +
+

+ + #visit_MultiTenant_TenantEnforcementClause(obj, collector) ⇒ Object + + + + + +

+
+ +

rubocop:disable Naming/MethodName

+ + +
+
+
+ + +
+ + + + +
+
+
+
+230
+231
+232
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 230
+
+def visit_MultiTenant_TenantEnforcementClause(obj, collector)
+  collector << obj
+end
+
+
+ +
+

+ + #visit_MultiTenant_TenantJoinEnforcementClause(obj, collector) ⇒ Object + + + + + +

+ + + + +
+
+
+
+234
+235
+236
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 234
+
+def visit_MultiTenant_TenantJoinEnforcementClause(obj, collector)
+  collector << obj
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenantFindBy.html b/docs/source/_static/api-reference/MultiTenantFindBy.html new file mode 100644 index 00000000..40ad0650 --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenantFindBy.html @@ -0,0 +1,180 @@ + + + + + + + Module: MultiTenantFindBy + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: MultiTenantFindBy + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/query_rewriter.rb
+
+ +
+ + + + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Instance Method Details

+ + +
+

+ + #cached_find_by_statement(key, &block) ⇒ Object + + + + + +

+ + + + +
+
+
+
+375
+376
+377
+378
+379
+380
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 375
+
+def cached_find_by_statement(key, &block)
+  return super unless respond_to?(:scoped_by_tenant?) && scoped_by_tenant?
+
+  key = Array.wrap(key) + [MultiTenant.current_tenant_id.to_s]
+  super(key, &block)
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/Sidekiq.html b/docs/source/_static/api-reference/Sidekiq.html new file mode 100644 index 00000000..5ecbd375 --- /dev/null +++ b/docs/source/_static/api-reference/Sidekiq.html @@ -0,0 +1,126 @@ + + + + + + + Module: Sidekiq + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: Sidekiq + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/sidekiq.rb
+
+ +
+ +

Overview

+
+ +

Bulk push support for Sidekiq while setting multi-tenant information. This is a copy of the Sidekiq::Client#push_bulk method with the addition of setting the multi-tenant information for each job.

+ + +
+
+
+ + +

Defined Under Namespace

+

+ + + + + Classes: Client + + +

+ + + + + + + + + +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/Sidekiq/Client.html b/docs/source/_static/api-reference/Sidekiq/Client.html new file mode 100644 index 00000000..1a18841b --- /dev/null +++ b/docs/source/_static/api-reference/Sidekiq/Client.html @@ -0,0 +1,302 @@ + + + + + + + Class: Sidekiq::Client + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: Sidekiq::Client + + + +

+
+ +
+
Inherits:
+
+ Object + +
    +
  • Object
  • + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/sidekiq.rb
+
+ +
+ + + + + + + + + +

+ Class Method Summary + collapse +

+ + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Class Method Details

+ + +
+

+ + .push_bulk_with_tenants(items) ⇒ Object + + + + + +

+ + + + +
+
+
+
+84
+85
+86
+
+
# File 'lib/activerecord-multi-tenant/sidekiq.rb', line 84
+
+def push_bulk_with_tenants(items)
+  new.push_bulk_with_tenants(items)
+end
+
+
+ +
+ +
+

Instance Method Details

+ + +
+

+ + #push_bulk_with_tenants(items) ⇒ Object + + + + + +

+
+ +

Allows the caller to enqueue multiple Sidekiq jobs with tenant information in a single call. It ensures that each job is processed within the correct tenant context and returns an array of job IDs for the enqueued jobs

+ + +
+
+
+ + +
+ + + + +
+
+
+
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+
+
# File 'lib/activerecord-multi-tenant/sidekiq.rb', line 62
+
+def push_bulk_with_tenants(items)
+  first_job = items['jobs'].first
+  return [] unless first_job # no jobs to push
+  unless first_job.is_a?(Hash)
+    raise ArgumentError, "Bulk arguments must be an Array of Hashes: [{ 'args' => [1], 'tenant_id' => 1 }, ...]"
+  end
+
+  normed = normalize_item(items.except('jobs').merge('args' => []))
+  payloads = items['jobs'].map do |job|
+    MultiTenant.with(job['tenant_id']) do
+      copy = normed.merge('args' => job['args'], 'jid' => SecureRandom.hex(12), 'enqueued_at' => Time.now.to_f)
+      result = process_single(items['class'], copy)
+      result || nil
+    end
+  end.compact
+
+  raw_push(payloads) unless payloads.empty?
+  payloads.collect { |payload| payload['jid'] }
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant.html b/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant.html new file mode 100644 index 00000000..e6b45f6b --- /dev/null +++ b/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant.html @@ -0,0 +1,126 @@ + + + + + + + Module: Sidekiq::Middleware::MultiTenant + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: Sidekiq::Middleware::MultiTenant + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/sidekiq.rb
+
+ +
+ +

Overview

+
+ +

Adds methods to handle tenant information both in the client and server.

+ + +
+
+
+ + +

Defined Under Namespace

+

+ + + + + Classes: Client, Server + + +

+ + + + + + + + + +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant/Client.html b/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant/Client.html new file mode 100644 index 00000000..bb036f05 --- /dev/null +++ b/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant/Client.html @@ -0,0 +1,217 @@ + + + + + + + Class: Sidekiq::Middleware::MultiTenant::Client + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: Sidekiq::Middleware::MultiTenant::Client + + + +

+
+ +
+
Inherits:
+
+ Object + +
    +
  • Object
  • + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/sidekiq.rb
+
+ +
+ +

Overview

+
+ +

Get the current tenant and store in the message to be sent to Sidekiq.

+ + +
+
+
+ + +
+ + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Instance Method Details

+ + +
+

+ + #call(_worker_class, msg, _queue, _redis_pool) ⇒ Object + + + + + +

+ + + + +
+
+
+
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+
+
# File 'lib/activerecord-multi-tenant/sidekiq.rb', line 7
+
+def call(_worker_class, msg, _queue, _redis_pool)
+  if MultiTenant.current_tenant.present?
+    msg['multi_tenant'] ||=
+      {
+        'class' => MultiTenant.current_tenant_class,
+        'id' => MultiTenant.current_tenant_id
+      }
+  end
+
+  yield
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant/Server.html b/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant/Server.html new file mode 100644 index 00000000..c2283eaf --- /dev/null +++ b/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant/Server.html @@ -0,0 +1,219 @@ + + + + + + + Class: Sidekiq::Middleware::MultiTenant::Server + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: Sidekiq::Middleware::MultiTenant::Server + + + +

+
+ +
+
Inherits:
+
+ Object + +
    +
  • Object
  • + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/sidekiq.rb
+
+ +
+ +

Overview

+
+ +

Pull the tenant out and run the current thread with it.

+ + +
+
+
+ + +
+ + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Instance Method Details

+ + +
+

+ + #call(_worker_class, msg, _queue, &block) ⇒ Object + + + + + +

+ + + + +
+
+
+
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+
+
# File 'lib/activerecord-multi-tenant/sidekiq.rb', line 22
+
+def call(_worker_class, msg, _queue, &block)
+  if msg.key?('multi_tenant')
+    tenant = begin
+      msg['multi_tenant']['class'].constantize.find(msg['multi_tenant']['id'])
+    rescue ActiveRecord::RecordNotFound
+      msg['multi_tenant']['id']
+    end
+    MultiTenant.with(tenant, &block)
+  else
+    yield
+  end
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/_index.html b/docs/source/_static/api-reference/_index.html new file mode 100644 index 00000000..ffaea9a3 --- /dev/null +++ b/docs/source/_static/api-reference/_index.html @@ -0,0 +1,399 @@ + + + + + + + Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Documentation by YARD 0.9.34

+
+

Alphabetic Index

+ +

File Listing

+ + +
+

Namespace Listing A-Z

+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/class_list.html b/docs/source/_static/api-reference/class_list.html new file mode 100644 index 00000000..c959f4e0 --- /dev/null +++ b/docs/source/_static/api-reference/class_list.html @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + Class List + + + +
+
+

Class List

+ + + +
+ + +
+ + diff --git a/docs/source/_static/api-reference/css/common.css b/docs/source/_static/api-reference/css/common.css new file mode 100644 index 00000000..cf25c452 --- /dev/null +++ b/docs/source/_static/api-reference/css/common.css @@ -0,0 +1 @@ +/* Override this file with custom rules */ \ No newline at end of file diff --git a/docs/source/_static/api-reference/css/full_list.css b/docs/source/_static/api-reference/css/full_list.css new file mode 100644 index 00000000..fa359824 --- /dev/null +++ b/docs/source/_static/api-reference/css/full_list.css @@ -0,0 +1,58 @@ +body { + margin: 0; + font-family: "Lucida Sans", "Lucida Grande", Verdana, Arial, sans-serif; + font-size: 13px; + height: 101%; + overflow-x: hidden; + background: #fafafa; +} + +h1 { padding: 12px 10px; padding-bottom: 0; margin: 0; font-size: 1.4em; } +.clear { clear: both; } +.fixed_header { position: fixed; background: #fff; width: 100%; padding-bottom: 10px; margin-top: 0; top: 0; z-index: 9999; height: 70px; } +#search { position: absolute; right: 5px; top: 9px; padding-left: 24px; } +#content.insearch #search, #content.insearch #noresults { background: url(data:image/gif;base64,R0lGODlhEAAQAPYAAP///wAAAPr6+pKSkoiIiO7u7sjIyNjY2J6engAAAI6OjsbGxjIyMlJSUuzs7KamppSUlPLy8oKCghwcHLKysqSkpJqamvT09Pj4+KioqM7OzkRERAwMDGBgYN7e3ujo6Ly8vCoqKjY2NkZGRtTU1MTExDw8PE5OTj4+PkhISNDQ0MrKylpaWrS0tOrq6nBwcKysrLi4uLq6ul5eXlxcXGJiYoaGhuDg4H5+fvz8/KKiohgYGCwsLFZWVgQEBFBQUMzMzDg4OFhYWBoaGvDw8NbW1pycnOLi4ubm5kBAQKqqqiQkJCAgIK6urnJyckpKSjQ0NGpqatLS0sDAwCYmJnx8fEJCQlRUVAoKCggICLCwsOTk5ExMTPb29ra2tmZmZmhoaNzc3KCgoBISEiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCAAAACwAAAAAEAAQAAAHaIAAgoMgIiYlg4kACxIaACEJCSiKggYMCRselwkpghGJBJEcFgsjJyoAGBmfggcNEx0flBiKDhQFlIoCCA+5lAORFb4AJIihCRbDxQAFChAXw9HSqb60iREZ1omqrIPdJCTe0SWI09GBACH5BAkIAAAALAAAAAAQABAAAAdrgACCgwc0NTeDiYozCQkvOTo9GTmDKy8aFy+NOBA7CTswgywJDTIuEjYFIY0JNYMtKTEFiRU8Pjwygy4ws4owPyCKwsMAJSTEgiQlgsbIAMrO0dKDGMTViREZ14kYGRGK38nHguHEJcvTyIEAIfkECQgAAAAsAAAAABAAEAAAB2iAAIKDAggPg4iJAAMJCRUAJRIqiRGCBI0WQEEJJkWDERkYAAUKEBc4Po1GiKKJHkJDNEeKig4URLS0ICImJZAkuQAhjSi/wQyNKcGDCyMnk8u5rYrTgqDVghgZlYjcACTA1sslvtHRgQAh+QQJCAAAACwAAAAAEAAQAAAHZ4AAgoOEhYaCJSWHgxGDJCQARAtOUoQRGRiFD0kJUYWZhUhKT1OLhR8wBaaFBzQ1NwAlkIszCQkvsbOHL7Y4q4IuEjaqq0ZQD5+GEEsJTDCMmIUhtgk1lo6QFUwJVDKLiYJNUd6/hoEAIfkECQgAAAAsAAAAABAAEAAAB2iAAIKDhIWGgiUlh4MRgyQkjIURGRiGGBmNhJWHm4uen4ICCA+IkIsDCQkVACWmhwSpFqAABQoQF6ALTkWFnYMrVlhWvIKTlSAiJiVVPqlGhJkhqShHV1lCW4cMqSkAR1ofiwsjJyqGgQAh+QQJCAAAACwAAAAAEAAQAAAHZ4AAgoOEhYaCJSWHgxGDJCSMhREZGIYYGY2ElYebi56fhyWQniSKAKKfpaCLFlAPhl0gXYNGEwkhGYREUywag1wJwSkHNDU3D0kJYIMZQwk8MjPBLx9eXwuETVEyAC/BOKsuEjYFhoEAIfkECQgAAAAsAAAAABAAEAAAB2eAAIKDhIWGgiUlh4MRgyQkjIURGRiGGBmNhJWHm4ueICImip6CIQkJKJ4kigynKaqKCyMnKqSEK05StgAGQRxPYZaENqccFgIID4KXmQBhXFkzDgOnFYLNgltaSAAEpxa7BQoQF4aBACH5BAkIAAAALAAAAAAQABAAAAdogACCg4SFggJiPUqCJSWGgkZjCUwZACQkgxGEXAmdT4UYGZqCGWQ+IjKGGIUwPzGPhAc0NTewhDOdL7Ykji+dOLuOLhI2BbaFETICx4MlQitdqoUsCQ2vhKGjglNfU0SWmILaj43M5oEAOwAAAAAAAAAAAA==) no-repeat center left; } +#full_list { padding: 0; list-style: none; margin-left: 0; margin-top: 80px; font-size: 1.1em; } +#full_list ul { padding: 0; } +#full_list li { padding: 0; margin: 0; list-style: none; } +#full_list li .item { padding: 5px 5px 5px 12px; } +#noresults { padding: 7px 12px; background: #fff; } +#content.insearch #noresults { margin-left: 7px; } +li.collapsed ul { display: none; } +li a.toggle { cursor: default; position: relative; left: -5px; top: 4px; text-indent: -999px; width: 10px; height: 9px; margin-left: -10px; display: block; float: left; background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAYAAABb0P4QAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAK8AAACvABQqw0mAAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTM5jWRgMAAAAVdEVYdENyZWF0aW9uIFRpbWUAMy8xNC8wOeNZPpQAAAE2SURBVDiNrZTBccIwEEXfelIAHUA6CZ24BGaWO+FuzZAK4k6gg5QAdGAq+Bxs2Yqx7BzyL7Llp/VfzZeQhCTc/ezuGzKKnKSzpCxXJM8fwNXda3df5RZETlIt6YUzSQDs93sl8w3wBZxCCE10GM1OcWbWjB2mWgEH4Mfdyxm3PSepBHibgQE2wLe7r4HjEidpnXMYdQPKEMJcsZ4zs2POYQOcaPfwMVOo58zsAdMt18BuoVDPxUJRacELbXv3hUIX2vYmOUvi8C8ydz/ThjXrqKqqLbDIAdsCKBd+Wo7GWa7o9qzOQHVVVXeAbs+yHHCH4aTsaCOQqunmUy1yBUAXkdMIfMlgF5EXLo2OpV/c/Up7jG4hhHcYLgWzAZXUc2b2ixsfvc/RmNNfOXD3Q/oeL9axJE1yT9IOoUu6MGUkAAAAAElFTkSuQmCC) no-repeat bottom left; } +li.collapsed a.toggle { opacity: 0.5; cursor: default; background-position: top left; } +li { color: #888; cursor: pointer; } +li.deprecated { text-decoration: line-through; font-style: italic; } +li.odd { background: #f0f0f0; } +li.even { background: #fafafa; } +.item:hover { background: #ddd; } +li small:before { content: "("; } +li small:after { content: ")"; } +li small.search_info { display: none; } +a, a:visited { text-decoration: none; color: #05a; } +li.clicked > .item { background: #05a; color: #ccc; } +li.clicked > .item a, li.clicked > .item a:visited { color: #eee; } +li.clicked > .item a.toggle { opacity: 0.5; background-position: bottom right; } +li.collapsed.clicked a.toggle { background-position: top right; } +#search input { border: 1px solid #bbb; border-radius: 3px; } +#full_list_nav { margin-left: 10px; font-size: 0.9em; display: block; color: #aaa; } +#full_list_nav a, #nav a:visited { color: #358; } +#full_list_nav a:hover { background: transparent; color: #5af; } +#full_list_nav span:after { content: ' | '; } +#full_list_nav span:last-child:after { content: ''; } + +#content h1 { margin-top: 0; } +li { white-space: nowrap; cursor: normal; } +li small { display: block; font-size: 0.8em; } +li small:before { content: ""; } +li small:after { content: ""; } +li small.search_info { display: none; } +#search { width: 170px; position: static; margin: 3px; margin-left: 10px; font-size: 0.9em; color: #888; padding-left: 0; padding-right: 24px; } +#content.insearch #search { background-position: center right; } +#search input { width: 110px; } + +#full_list.insearch ul { display: block; } +#full_list.insearch .item { display: none; } +#full_list.insearch .found { display: block; padding-left: 11px !important; } +#full_list.insearch li a.toggle { display: none; } +#full_list.insearch li small.search_info { display: block; } diff --git a/docs/source/_static/api-reference/css/style.css b/docs/source/_static/api-reference/css/style.css new file mode 100644 index 00000000..eb0dbc86 --- /dev/null +++ b/docs/source/_static/api-reference/css/style.css @@ -0,0 +1,497 @@ +html { + width: 100%; + height: 100%; +} +body { + font-family: "Lucida Sans", "Lucida Grande", Verdana, Arial, sans-serif; + font-size: 13px; + width: 100%; + margin: 0; + padding: 0; + display: flex; + display: -webkit-flex; + display: -ms-flexbox; +} + +#nav { + position: relative; + width: 100%; + height: 100%; + border: 0; + border-right: 1px dotted #eee; + overflow: auto; +} +.nav_wrap { + margin: 0; + padding: 0; + width: 20%; + height: 100%; + position: relative; + display: flex; + display: -webkit-flex; + display: -ms-flexbox; + flex-shrink: 0; + -webkit-flex-shrink: 0; + -ms-flex: 1 0; +} +#resizer { + position: absolute; + right: -5px; + top: 0; + width: 10px; + height: 100%; + cursor: col-resize; + z-index: 9999; +} +#main { + flex: 5 1; + -webkit-flex: 5 1; + -ms-flex: 5 1; + outline: none; + position: relative; + background: #fff; + padding: 1.2em; + padding-top: 0.2em; + box-sizing: border-box; +} + +@media (max-width: 920px) { + .nav_wrap { width: 100%; top: 0; right: 0; overflow: visible; position: absolute; } + #resizer { display: none; } + #nav { + z-index: 9999; + background: #fff; + display: none; + position: absolute; + top: 40px; + right: 12px; + width: 500px; + max-width: 80%; + height: 80%; + overflow-y: scroll; + border: 1px solid #999; + border-collapse: collapse; + box-shadow: -7px 5px 25px #aaa; + border-radius: 2px; + } +} + +@media (min-width: 920px) { + body { height: 100%; overflow: hidden; } + #main { height: 100%; overflow: auto; } + #search { display: none; } +} + +#main img { max-width: 100%; } +h1 { font-size: 25px; margin: 1em 0 0.5em; padding-top: 4px; border-top: 1px dotted #d5d5d5; } +h1.noborder { border-top: 0px; margin-top: 0; padding-top: 4px; } +h1.title { margin-bottom: 10px; } +h1.alphaindex { margin-top: 0; font-size: 22px; } +h2 { + padding: 0; + padding-bottom: 3px; + border-bottom: 1px #aaa solid; + font-size: 1.4em; + margin: 1.8em 0 0.5em; + position: relative; +} +h2 small { font-weight: normal; font-size: 0.7em; display: inline; position: absolute; right: 0; } +h2 small a { + display: block; + height: 20px; + border: 1px solid #aaa; + border-bottom: 0; + border-top-left-radius: 5px; + background: #f8f8f8; + position: relative; + padding: 2px 7px; +} +.clear { clear: both; } +.inline { display: inline; } +.inline p:first-child { display: inline; } +.docstring, .tags, #filecontents { font-size: 15px; line-height: 1.5145em; } +.docstring p > code, .docstring p > tt, .tags p > code, .tags p > tt { + color: #c7254e; background: #f9f2f4; padding: 2px 4px; font-size: 1em; + border-radius: 4px; +} +.docstring h1, .docstring h2, .docstring h3, .docstring h4 { padding: 0; border: 0; border-bottom: 1px dotted #bbb; } +.docstring h1 { font-size: 1.2em; } +.docstring h2 { font-size: 1.1em; } +.docstring h3, .docstring h4 { font-size: 1em; border-bottom: 0; padding-top: 10px; } +.summary_desc .object_link a, .docstring .object_link a { + font-family: monospace; font-size: 1.05em; + color: #05a; background: #EDF4FA; padding: 2px 4px; font-size: 1em; + border-radius: 4px; +} +.rdoc-term { padding-right: 25px; font-weight: bold; } +.rdoc-list p { margin: 0; padding: 0; margin-bottom: 4px; } +.summary_desc pre.code .object_link a, .docstring pre.code .object_link a { + padding: 0px; background: inherit; color: inherit; border-radius: inherit; +} + +/* style for */ +#filecontents table, .docstring table { border-collapse: collapse; } +#filecontents table th, #filecontents table td, +.docstring table th, .docstring table td { border: 1px solid #ccc; padding: 8px; padding-right: 17px; } +#filecontents table tr:nth-child(odd), +.docstring table tr:nth-child(odd) { background: #eee; } +#filecontents table tr:nth-child(even), +.docstring table tr:nth-child(even) { background: #fff; } +#filecontents table th, .docstring table th { background: #fff; } + +/* style for
    */ +#filecontents li > p, .docstring li > p { margin: 0px; } +#filecontents ul, .docstring ul { padding-left: 20px; } +/* style for
    */ +#filecontents dl, .docstring dl { border: 1px solid #ccc; } +#filecontents dt, .docstring dt { background: #ddd; font-weight: bold; padding: 3px 5px; } +#filecontents dd, .docstring dd { padding: 5px 0px; margin-left: 18px; } +#filecontents dd > p, .docstring dd > p { margin: 0px; } + +.note { + color: #222; + margin: 20px 0; + padding: 10px; + border: 1px solid #eee; + border-radius: 3px; + display: block; +} +.docstring .note { + border-left-color: #ccc; + border-left-width: 5px; +} +.note.todo { background: #ffffc5; border-color: #ececaa; } +.note.returns_void { background: #efefef; } +.note.deprecated { background: #ffe5e5; border-color: #e9dada; } +.note.title.deprecated { background: #ffe5e5; border-color: #e9dada; } +.note.private { background: #ffffc5; border-color: #ececaa; } +.note.title { padding: 3px 6px; font-size: 0.9em; font-family: "Lucida Sans", "Lucida Grande", Verdana, Arial, sans-serif; display: inline; } +.summary_signature + .note.title { margin-left: 7px; } +h1 .note.title { font-size: 0.5em; font-weight: normal; padding: 3px 5px; position: relative; top: -3px; text-transform: capitalize; } +.note.title { background: #efefef; } +.note.title.constructor { color: #fff; background: #6a98d6; border-color: #6689d6; } +.note.title.writeonly { color: #fff; background: #45a638; border-color: #2da31d; } +.note.title.readonly { color: #fff; background: #6a98d6; border-color: #6689d6; } +.note.title.private { background: #d5d5d5; border-color: #c5c5c5; } +.note.title.not_defined_here { background: transparent; border: none; font-style: italic; } +.discussion .note { margin-top: 6px; } +.discussion .note:first-child { margin-top: 0; } + +h3.inherited { + font-style: italic; + font-family: "Lucida Sans", "Lucida Grande", Verdana, Arial, sans-serif; + font-weight: normal; + padding: 0; + margin: 0; + margin-top: 12px; + margin-bottom: 3px; + font-size: 13px; +} +p.inherited { + padding: 0; + margin: 0; + margin-left: 25px; +} + +.box_info dl { + margin: 0; + border: 0; + width: 100%; + font-size: 1em; + display: flex; + display: -webkit-flex; + display: -ms-flexbox; +} +.box_info dl dt { + flex-shrink: 0; + -webkit-flex-shrink: 1; + -ms-flex-shrink: 1; + width: 100px; + text-align: right; + font-weight: bold; + border: 1px solid #aaa; + border-width: 1px 0px 0px 1px; + padding: 6px 0; + padding-right: 10px; +} +.box_info dl dd { + flex-grow: 1; + -webkit-flex-grow: 1; + -ms-flex: 1; + max-width: 420px; + padding: 6px 0; + padding-right: 20px; + border: 1px solid #aaa; + border-width: 1px 1px 0 0; + overflow: hidden; + position: relative; +} +.box_info dl:last-child > * { + border-bottom: 1px solid #aaa; +} +.box_info dl:nth-child(odd) > * { background: #eee; } +.box_info dl:nth-child(even) > * { background: #fff; } +.box_info dl > * { margin: 0; } + +ul.toplevel { list-style: none; padding-left: 0; font-size: 1.1em; } +.index_inline_list { padding-left: 0; font-size: 1.1em; } + +.index_inline_list li { + list-style: none; + display: inline-block; + padding: 0 12px; + line-height: 30px; + margin-bottom: 5px; +} + +dl.constants { margin-left: 10px; } +dl.constants dt { font-weight: bold; font-size: 1.1em; margin-bottom: 5px; } +dl.constants.compact dt { display: inline-block; font-weight: normal } +dl.constants dd { width: 75%; white-space: pre; font-family: monospace; margin-bottom: 18px; } +dl.constants .docstring .note:first-child { margin-top: 5px; } + +.summary_desc { + margin-left: 32px; + display: block; + font-family: sans-serif; + font-size: 1.1em; + margin-top: 8px; + line-height: 1.5145em; + margin-bottom: 0.8em; +} +.summary_desc tt { font-size: 0.9em; } +dl.constants .note { padding: 2px 6px; padding-right: 12px; margin-top: 6px; } +dl.constants .docstring { margin-left: 32px; font-size: 0.9em; font-weight: normal; } +dl.constants .tags { padding-left: 32px; font-size: 0.9em; line-height: 0.8em; } +dl.constants .discussion *:first-child { margin-top: 0; } +dl.constants .discussion *:last-child { margin-bottom: 0; } + +.method_details { border-top: 1px dotted #ccc; margin-top: 25px; padding-top: 0; } +.method_details.first { border: 0; margin-top: 5px; } +.method_details.first h3.signature { margin-top: 1em; } +p.signature, h3.signature { + font-size: 1.1em; font-weight: normal; font-family: Monaco, Consolas, Courier, monospace; + padding: 6px 10px; margin-top: 1em; + background: #E8F4FF; border: 1px solid #d8d8e5; border-radius: 5px; +} +p.signature tt, +h3.signature tt { font-family: Monaco, Consolas, Courier, monospace; } +p.signature .overload, +h3.signature .overload { display: block; } +p.signature .extras, +h3.signature .extras { font-weight: normal; font-family: sans-serif; color: #444; font-size: 1em; } +p.signature .not_defined_here, +h3.signature .not_defined_here, +p.signature .aliases, +h3.signature .aliases { display: block; font-weight: normal; font-size: 0.9em; font-family: sans-serif; margin-top: 0px; color: #555; } +p.signature .aliases .names, +h3.signature .aliases .names { font-family: Monaco, Consolas, Courier, monospace; font-weight: bold; color: #000; font-size: 1.2em; } + +.tags .tag_title { font-size: 1.05em; margin-bottom: 0; font-weight: bold; } +.tags .tag_title tt { color: initial; padding: initial; background: initial; } +.tags ul { margin-top: 5px; padding-left: 30px; list-style: square; } +.tags ul li { margin-bottom: 3px; } +.tags ul .name { font-family: monospace; font-weight: bold; } +.tags ul .note { padding: 3px 6px; } +.tags { margin-bottom: 12px; } + +.tags .examples .tag_title { margin-bottom: 10px; font-weight: bold; } +.tags .examples .inline p { padding: 0; margin: 0; font-weight: bold; font-size: 1em; } +.tags .examples .inline p:before { content: "▸"; font-size: 1em; margin-right: 5px; } + +.tags .overload .overload_item { list-style: none; margin-bottom: 25px; } +.tags .overload .overload_item .signature { + padding: 2px 8px; + background: #F1F8FF; border: 1px solid #d8d8e5; border-radius: 3px; +} +.tags .overload .signature { margin-left: -15px; font-family: monospace; display: block; font-size: 1.1em; } +.tags .overload .docstring { margin-top: 15px; } + +.defines { display: none; } + +#method_missing_details .notice.this { position: relative; top: -8px; color: #888; padding: 0; margin: 0; } + +.showSource { font-size: 0.9em; } +.showSource a, .showSource a:visited { text-decoration: none; color: #666; } + +#content a, #content a:visited { text-decoration: none; color: #05a; } +#content a:hover { background: #ffffa5; } + +ul.summary { + list-style: none; + font-family: monospace; + font-size: 1em; + line-height: 1.5em; + padding-left: 0px; +} +ul.summary a, ul.summary a:visited { + text-decoration: none; font-size: 1.1em; +} +ul.summary li { margin-bottom: 5px; } +.summary_signature { padding: 4px 8px; background: #f8f8f8; border: 1px solid #f0f0f0; border-radius: 5px; } +.summary_signature:hover { background: #CFEBFF; border-color: #A4CCDA; cursor: pointer; } +.summary_signature.deprecated { background: #ffe5e5; border-color: #e9dada; } +ul.summary.compact li { display: inline-block; margin: 0px 5px 0px 0px; line-height: 2.6em;} +ul.summary.compact .summary_signature { padding: 5px 7px; padding-right: 4px; } +#content .summary_signature:hover a, +#content .summary_signature:hover a:visited { + background: transparent; + color: #049; +} + +p.inherited a { font-family: monospace; font-size: 0.9em; } +p.inherited { word-spacing: 5px; font-size: 1.2em; } + +p.children { font-size: 1.2em; } +p.children a { font-size: 0.9em; } +p.children strong { font-size: 0.8em; } +p.children strong.modules { padding-left: 5px; } + +ul.fullTree { display: none; padding-left: 0; list-style: none; margin-left: 0; margin-bottom: 10px; } +ul.fullTree ul { margin-left: 0; padding-left: 0; list-style: none; } +ul.fullTree li { text-align: center; padding-top: 18px; padding-bottom: 12px; background: url(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAAHtJREFUeNqMzrEJAkEURdGzuhgZbSoYWcAWoBVsB4JgZAGmphsZCZYzTQgWNCYrDN9RvMmHx+X916SUBFbo8CzD1idXrLErw1mQttgXtyrOcQ/Ny5p4Qh+2XqLYYazsPWNTiuMkRxa4vcV+evuNAUOLIx5+c2hyzv7hNQC67Q+/HHmlEwAAAABJRU5ErkJggg==) no-repeat top center; } +ul.fullTree li:first-child { padding-top: 0; background: transparent; } +ul.fullTree li:last-child { padding-bottom: 0; } +.showAll ul.fullTree { display: block; } +.showAll .inheritName { display: none; } + +#search { position: absolute; right: 12px; top: 0px; z-index: 9000; } +#search a { + display: block; float: left; + padding: 4px 8px; text-decoration: none; color: #05a; fill: #05a; + border: 1px solid #d8d8e5; + border-bottom-left-radius: 3px; border-bottom-right-radius: 3px; + background: #F1F8FF; + box-shadow: -1px 1px 3px #ddd; +} +#search a:hover { background: #f5faff; color: #06b; fill: #06b; } +#search a.active { + background: #568; padding-bottom: 20px; color: #fff; fill: #fff; + border: 1px solid #457; + border-top-left-radius: 5px; border-top-right-radius: 5px; +} +#search a.inactive { color: #999; fill: #999; } +.inheritanceTree, .toggleDefines { + float: right; + border-left: 1px solid #aaa; + position: absolute; top: 0; right: 0; + height: 100%; + background: #f6f6f6; + padding: 5px; + min-width: 55px; + text-align: center; +} + +#menu { font-size: 1.3em; color: #bbb; } +#menu .title, #menu a { font-size: 0.7em; } +#menu .title a { font-size: 1em; } +#menu .title { color: #555; } +#menu a, #menu a:visited { color: #333; text-decoration: none; border-bottom: 1px dotted #bbd; } +#menu a:hover { color: #05a; } + +#footer { margin-top: 15px; border-top: 1px solid #ccc; text-align: center; padding: 7px 0; color: #999; } +#footer a, #footer a:visited { color: #444; text-decoration: none; border-bottom: 1px dotted #bbd; } +#footer a:hover { color: #05a; } + +#listing ul.alpha { font-size: 1.1em; } +#listing ul.alpha { margin: 0; padding: 0; padding-bottom: 10px; list-style: none; } +#listing ul.alpha li.letter { font-size: 1.4em; padding-bottom: 10px; } +#listing ul.alpha ul { margin: 0; padding-left: 15px; } +#listing ul small { color: #666; font-size: 0.7em; } + +li.r1 { background: #f0f0f0; } +li.r2 { background: #fafafa; } + +#content ul.summary li.deprecated .summary_signature a, +#content ul.summary li.deprecated .summary_signature a:visited { text-decoration: line-through; font-style: italic; } + +#toc { + position: relative; + float: right; + overflow-x: auto; + right: -3px; + margin-left: 20px; + margin-bottom: 20px; + padding: 20px; padding-right: 30px; + max-width: 300px; + z-index: 5000; + background: #fefefe; + border: 1px solid #ddd; + box-shadow: -2px 2px 6px #bbb; +} +#toc .title { margin: 0; } +#toc ol { padding-left: 1.8em; } +#toc li { font-size: 1.1em; line-height: 1.7em; } +#toc > ol > li { font-size: 1.1em; font-weight: bold; } +#toc ol > li > ol { font-size: 0.9em; } +#toc ol ol > li > ol { padding-left: 2.3em; } +#toc ol + li { margin-top: 0.3em; } +#toc.hidden { padding: 10px; background: #fefefe; box-shadow: none; } +#toc.hidden:hover { background: #fafafa; } +#filecontents h1 + #toc.nofloat { margin-top: 0; } +@media (max-width: 560px) { + #toc { + margin-left: 0; + margin-top: 16px; + float: none; + max-width: none; + } +} + +/* syntax highlighting */ +.source_code { display: none; padding: 3px 8px; border-left: 8px solid #ddd; margin-top: 5px; } +#filecontents pre.code, .docstring pre.code, .source_code pre { font-family: monospace; } +#filecontents pre.code, .docstring pre.code { display: block; } +.source_code .lines { padding-right: 12px; color: #555; text-align: right; } +#filecontents pre.code, .docstring pre.code, +.tags pre.example { + padding: 9px 14px; + margin-top: 4px; + border: 1px solid #e1e1e8; + background: #f7f7f9; + border-radius: 4px; + font-size: 1em; + overflow-x: auto; + line-height: 1.2em; +} +pre.code { color: #000; tab-size: 2; } +pre.code .info.file { color: #555; } +pre.code .val { color: #036A07; } +pre.code .tstring_content, +pre.code .heredoc_beg, pre.code .heredoc_end, +pre.code .qwords_beg, pre.code .qwords_end, pre.code .qwords_sep, +pre.code .words_beg, pre.code .words_end, pre.code .words_sep, +pre.code .qsymbols_beg, pre.code .qsymbols_end, pre.code .qsymbols_sep, +pre.code .symbols_beg, pre.code .symbols_end, pre.code .symbols_sep, +pre.code .tstring, pre.code .dstring { color: #036A07; } +pre.code .fid, pre.code .rubyid_new, pre.code .rubyid_to_s, +pre.code .rubyid_to_sym, pre.code .rubyid_to_f, +pre.code .dot + pre.code .id, +pre.code .rubyid_to_i pre.code .rubyid_each { color: #0085FF; } +pre.code .comment { color: #0066FF; } +pre.code .const, pre.code .constant { color: #585CF6; } +pre.code .label, +pre.code .symbol { color: #C5060B; } +pre.code .kw, +pre.code .rubyid_require, +pre.code .rubyid_extend, +pre.code .rubyid_include { color: #0000FF; } +pre.code .ivar { color: #318495; } +pre.code .gvar, +pre.code .rubyid_backref, +pre.code .rubyid_nth_ref { color: #6D79DE; } +pre.code .regexp, .dregexp { color: #036A07; } +pre.code a { border-bottom: 1px dotted #bbf; } +/* inline code */ +*:not(pre) > code { + padding: 1px 3px 1px 3px; + border: 1px solid #E1E1E8; + background: #F7F7F9; + border-radius: 4px; +} + +/* Color fix for links */ +#content .summary_desc pre.code .id > .object_link a, /* identifier */ +#content .docstring pre.code .id > .object_link a { color: #0085FF; } +#content .summary_desc pre.code .const > .object_link a, /* constant */ +#content .docstring pre.code .const > .object_link a { color: #585CF6; } diff --git a/docs/source/_static/api-reference/file.README.html b/docs/source/_static/api-reference/file.README.html new file mode 100644 index 00000000..222829ae --- /dev/null +++ b/docs/source/_static/api-reference/file.README.html @@ -0,0 +1,167 @@ + + + + + + + File: README + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
    + + +
    +

    activerecord-multi-tenant rdoc-image:https://img.shields.io/gem/v/activerecord-multi-tenant.svg rdoc-image:https://img.shields.io/gem/dt/activerecord-multi-tenant.svg

    + +

    Introduction Post: www.citusdata.com/blog/2017/01/05/easily-scale-out-multi-tenant-apps/

    + +

    ActiveRecord/Rails integration for multi-tenant databases, in particular the open-source Citus extension for PostgreSQL.

    + +

    Enables easy scale-out by adding the tenant context to your queries, enabling the database (e.g. Citus) to efficiently route queries to the right database node.

    + +

    Installation

    + +

    Add the following to your Gemfile:

    + +
    gem 'activerecord-multi-tenant'
    +
    + +

    Supported Rails versions

    + +

    All Ruby on Rails versions starting with 6.0 or newer (up to 7.0) are supported.

    + +

    This gem only supports ActiveRecord (the Rails default ORM), and not alternative ORMs like Sequel.

    + +

    Usage

    + +

    It is required that you add multi_tenant definitions to your model in order to have full support for Citus, in particular when updating records.

    + +

    In the example of an analytics application, sharding on customer_id, annotate your models like this:

    + +
    class PageView < ActiveRecord::Base
    +  multi_tenant :customer
    +  belongs_to :site
    +
    +  # ...
    +end
    +
    +class Site < ActiveRecord::Base
    +  multi_tenant :customer
    +  has_many :page_views
    +
    +  # ...
    +end
    +
    + +

    and then wrap all code that runs queries/modifications in blocks like this:

    + +
    customer = Customer.find(session[:current_customer_id])
    +# ...
    +MultiTenant.with(customer) do
    +  site = Site.find(params[:site_id])
    +  site.update! last_accessed_at: Time.now
    +  site.page_views.count
    +end
    +
    + +

    Inside controllers you can use a before_action together with set_current_tenant, to set the tenant for the current request:

    + +
    class ApplicationController < ActionController::Base
    +  set_current_tenant_through_filter # Required to opt into this behavior
    +  before_action :set_customer_as_tenant
    +
    +  def set_customer_as_tenant
    +    customer = Customer.find(session[:current_customer_id])
    +    set_current_tenant(customer)
    +  end
    +end
    +
    + +

    Rolling out activerecord-multi-tenant for your application (write-only mode)

    + +

    The library relies on tenant_id to be present and NOT NULL for all rows. However, its often useful to have the library set the tenant_id for new records, and then backfilling tenant_id for existing records as a background task.

    + +

    To support this, there is a write-only mode, in which tenant_id is not included in queries, but only set for new records. Include the following in an initializer to enable it:

    + +
    MultiTenant.enable_write_only_mode
    +
    + +

    Once you are ready to enforce tenancy, make your tenant_id column NOT NULL and simply remove that line.

    + +

    Frequently Asked Questions

    +
    • +

      What if I have a table that doesn’t relate to my tenant? (e.g. templates that are the same in every account)

      +
    + +

    We recommend not using activerecord-multi-tenant on these tables. In case only some records in a table are not associated to a tenant (i.e. your templates are in the same table as actual objects), we recommend setting the tenant_id to 0, and then using MultiTenant.with(0) to access these objects.

    +
    • +

      What if my tenant model is not defined in my application?

      +
    + +

    The tenant model does not have to be defined. Use the gem as if the model was present. MultiTenant.with accepts either a tenant id or model instance.

    + +

    Credits

    + +

    This gem was initially based on acts_as_tenant, and still shares some code. We thank the authors for their efforts.

    + +

    License

    + +

    Copyright © 2018, Citus Data Inc.
    Licensed under the MIT license, see LICENSE file for details.

    +
    + + + +
    + + \ No newline at end of file diff --git a/docs/source/_static/api-reference/file_list.html b/docs/source/_static/api-reference/file_list.html new file mode 100644 index 00000000..2b6df404 --- /dev/null +++ b/docs/source/_static/api-reference/file_list.html @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + File List + + + +
    +
    +

    File List

    + + + +
    + + +
    + + diff --git a/docs/source/_static/api-reference/frames.html b/docs/source/_static/api-reference/frames.html new file mode 100644 index 00000000..4f918f57 --- /dev/null +++ b/docs/source/_static/api-reference/frames.html @@ -0,0 +1,17 @@ + + + + + Documentation by YARD 0.9.34 + + + + diff --git a/docs/source/_static/api-reference/index.html b/docs/source/_static/api-reference/index.html new file mode 100644 index 00000000..cf60c3ab --- /dev/null +++ b/docs/source/_static/api-reference/index.html @@ -0,0 +1,167 @@ + + + + + + + File: README + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
    + + +
    +

    activerecord-multi-tenant rdoc-image:https://img.shields.io/gem/v/activerecord-multi-tenant.svg rdoc-image:https://img.shields.io/gem/dt/activerecord-multi-tenant.svg

    + +

    Introduction Post: www.citusdata.com/blog/2017/01/05/easily-scale-out-multi-tenant-apps/

    + +

    ActiveRecord/Rails integration for multi-tenant databases, in particular the open-source Citus extension for PostgreSQL.

    + +

    Enables easy scale-out by adding the tenant context to your queries, enabling the database (e.g. Citus) to efficiently route queries to the right database node.

    + +

    Installation

    + +

    Add the following to your Gemfile:

    + +
    gem 'activerecord-multi-tenant'
    +
    + +

    Supported Rails versions

    + +

    All Ruby on Rails versions starting with 6.0 or newer (up to 7.0) are supported.

    + +

    This gem only supports ActiveRecord (the Rails default ORM), and not alternative ORMs like Sequel.

    + +

    Usage

    + +

    It is required that you add multi_tenant definitions to your model in order to have full support for Citus, in particular when updating records.

    + +

    In the example of an analytics application, sharding on customer_id, annotate your models like this:

    + +
    class PageView < ActiveRecord::Base
    +  multi_tenant :customer
    +  belongs_to :site
    +
    +  # ...
    +end
    +
    +class Site < ActiveRecord::Base
    +  multi_tenant :customer
    +  has_many :page_views
    +
    +  # ...
    +end
    +
    + +

    and then wrap all code that runs queries/modifications in blocks like this:

    + +
    customer = Customer.find(session[:current_customer_id])
    +# ...
    +MultiTenant.with(customer) do
    +  site = Site.find(params[:site_id])
    +  site.update! last_accessed_at: Time.now
    +  site.page_views.count
    +end
    +
    + +

    Inside controllers you can use a before_action together with set_current_tenant, to set the tenant for the current request:

    + +
    class ApplicationController < ActionController::Base
    +  set_current_tenant_through_filter # Required to opt into this behavior
    +  before_action :set_customer_as_tenant
    +
    +  def set_customer_as_tenant
    +    customer = Customer.find(session[:current_customer_id])
    +    set_current_tenant(customer)
    +  end
    +end
    +
    + +

    Rolling out activerecord-multi-tenant for your application (write-only mode)

    + +

    The library relies on tenant_id to be present and NOT NULL for all rows. However, its often useful to have the library set the tenant_id for new records, and then backfilling tenant_id for existing records as a background task.

    + +

    To support this, there is a write-only mode, in which tenant_id is not included in queries, but only set for new records. Include the following in an initializer to enable it:

    + +
    MultiTenant.enable_write_only_mode
    +
    + +

    Once you are ready to enforce tenancy, make your tenant_id column NOT NULL and simply remove that line.

    + +

    Frequently Asked Questions

    +
    • +

      What if I have a table that doesn’t relate to my tenant? (e.g. templates that are the same in every account)

      +
    + +

    We recommend not using activerecord-multi-tenant on these tables. In case only some records in a table are not associated to a tenant (i.e. your templates are in the same table as actual objects), we recommend setting the tenant_id to 0, and then using MultiTenant.with(0) to access these objects.

    +
    • +

      What if my tenant model is not defined in my application?

      +
    + +

    The tenant model does not have to be defined. Use the gem as if the model was present. MultiTenant.with accepts either a tenant id or model instance.

    + +

    Credits

    + +

    This gem was initially based on acts_as_tenant, and still shares some code. We thank the authors for their efforts.

    + +

    License

    + +

    Copyright © 2018, Citus Data Inc.
    Licensed under the MIT license, see LICENSE file for details.

    +
    + + + +
    + + \ No newline at end of file diff --git a/docs/source/_static/api-reference/js/app.js b/docs/source/_static/api-reference/js/app.js new file mode 100644 index 00000000..8d067fe3 --- /dev/null +++ b/docs/source/_static/api-reference/js/app.js @@ -0,0 +1,314 @@ +(function() { + +var localStorage = {}, sessionStorage = {}; +try { localStorage = window.localStorage; } catch (e) { } +try { sessionStorage = window.sessionStorage; } catch (e) { } + +function createSourceLinks() { + $('.method_details_list .source_code'). + before("[View source]"); + $('.toggleSource').toggle(function() { + $(this).parent().nextAll('.source_code').slideDown(100); + $(this).text("Hide source"); + }, + function() { + $(this).parent().nextAll('.source_code').slideUp(100); + $(this).text("View source"); + }); +} + +function createDefineLinks() { + var tHeight = 0; + $('.defines').after(" more..."); + $('.toggleDefines').toggle(function() { + tHeight = $(this).parent().prev().height(); + $(this).prev().css('display', 'inline'); + $(this).parent().prev().height($(this).parent().height()); + $(this).text("(less)"); + }, + function() { + $(this).prev().hide(); + $(this).parent().prev().height(tHeight); + $(this).text("more..."); + }); +} + +function createFullTreeLinks() { + var tHeight = 0; + $('.inheritanceTree').toggle(function() { + tHeight = $(this).parent().prev().height(); + $(this).parent().toggleClass('showAll'); + $(this).text("(hide)"); + $(this).parent().prev().height($(this).parent().height()); + }, + function() { + $(this).parent().toggleClass('showAll'); + $(this).parent().prev().height(tHeight); + $(this).text("show all"); + }); +} + +function searchFrameButtons() { + $('.full_list_link').click(function() { + toggleSearchFrame(this, $(this).attr('href')); + return false; + }); + window.addEventListener('message', function(e) { + if (e.data === 'navEscape') { + $('#nav').slideUp(100); + $('#search a').removeClass('active inactive'); + $(window).focus(); + } + }); + + $(window).resize(function() { + if ($('#search:visible').length === 0) { + $('#nav').removeAttr('style'); + $('#search a').removeClass('active inactive'); + $(window).focus(); + } + }); +} + +function toggleSearchFrame(id, link) { + var frame = $('#nav'); + $('#search a').removeClass('active').addClass('inactive'); + if (frame.attr('src') === link && frame.css('display') !== "none") { + frame.slideUp(100); + $('#search a').removeClass('active inactive'); + } + else { + $(id).addClass('active').removeClass('inactive'); + if (frame.attr('src') !== link) frame.attr('src', link); + frame.slideDown(100); + } +} + +function linkSummaries() { + $('.summary_signature').click(function() { + document.location = $(this).find('a').attr('href'); + }); +} + +function summaryToggle() { + $('.summary_toggle').click(function(e) { + e.preventDefault(); + localStorage.summaryCollapsed = $(this).text(); + $('.summary_toggle').each(function() { + $(this).text($(this).text() == "collapse" ? "expand" : "collapse"); + var next = $(this).parent().parent().nextAll('ul.summary').first(); + if (next.hasClass('compact')) { + next.toggle(); + next.nextAll('ul.summary').first().toggle(); + } + else if (next.hasClass('summary')) { + var list = $('
      '); + list.html(next.html()); + list.find('.summary_desc, .note').remove(); + list.find('a').each(function() { + $(this).html($(this).find('strong').html()); + $(this).parent().html($(this)[0].outerHTML); + }); + next.before(list); + next.toggle(); + } + }); + return false; + }); + if (localStorage.summaryCollapsed == "collapse") { + $('.summary_toggle').first().click(); + } else { localStorage.summaryCollapsed = "expand"; } +} + +function constantSummaryToggle() { + $('.constants_summary_toggle').click(function(e) { + e.preventDefault(); + localStorage.summaryCollapsed = $(this).text(); + $('.constants_summary_toggle').each(function() { + $(this).text($(this).text() == "collapse" ? "expand" : "collapse"); + var next = $(this).parent().parent().nextAll('dl.constants').first(); + if (next.hasClass('compact')) { + next.toggle(); + next.nextAll('dl.constants').first().toggle(); + } + else if (next.hasClass('constants')) { + var list = $('
      '); + list.html(next.html()); + list.find('dt').each(function() { + $(this).addClass('summary_signature'); + $(this).text( $(this).text().split('=')[0]); + if ($(this).has(".deprecated").length) { + $(this).addClass('deprecated'); + }; + }); + // Add the value of the constant as "Tooltip" to the summary object + list.find('pre.code').each(function() { + console.log($(this).parent()); + var dt_element = $(this).parent().prev(); + var tooltip = $(this).text(); + if (dt_element.hasClass("deprecated")) { + tooltip = 'Deprecated. ' + tooltip; + }; + dt_element.attr('title', tooltip); + }); + list.find('.docstring, .tags, dd').remove(); + next.before(list); + next.toggle(); + } + }); + return false; + }); + if (localStorage.summaryCollapsed == "collapse") { + $('.constants_summary_toggle').first().click(); + } else { localStorage.summaryCollapsed = "expand"; } +} + +function generateTOC() { + if ($('#filecontents').length === 0) return; + var _toc = $('
        '); + var show = false; + var toc = _toc; + var counter = 0; + var tags = ['h2', 'h3', 'h4', 'h5', 'h6']; + var i; + var curli; + if ($('#filecontents h1').length > 1) tags.unshift('h1'); + for (i = 0; i < tags.length; i++) { tags[i] = '#filecontents ' + tags[i]; } + var lastTag = parseInt(tags[0][1], 10); + $(tags.join(', ')).each(function() { + if ($(this).parents('.method_details .docstring').length != 0) return; + if (this.id == "filecontents") return; + show = true; + var thisTag = parseInt(this.tagName[1], 10); + if (this.id.length === 0) { + var proposedId = $(this).attr('toc-id'); + if (typeof(proposedId) != "undefined") this.id = proposedId; + else { + var proposedId = $(this).text().replace(/[^a-z0-9-]/ig, '_'); + if ($('#' + proposedId).length > 0) { proposedId += counter; counter++; } + this.id = proposedId; + } + } + if (thisTag > lastTag) { + for (i = 0; i < thisTag - lastTag; i++) { + if ( typeof(curli) == "undefined" ) { + curli = $('
      1. '); + toc.append(curli); + } + toc = $('
          '); + curli.append(toc); + curli = undefined; + } + } + if (thisTag < lastTag) { + for (i = 0; i < lastTag - thisTag; i++) { + toc = toc.parent(); + toc = toc.parent(); + } + } + var title = $(this).attr('toc-title'); + if (typeof(title) == "undefined") title = $(this).text(); + curli =$('
        1. ' + title + '
        2. '); + toc.append(curli); + lastTag = thisTag; + }); + if (!show) return; + html = ''; + $('#content').prepend(html); + $('#toc').append(_toc); + $('#toc .hide_toc').toggle(function() { + $('#toc .top').slideUp('fast'); + $('#toc').toggleClass('hidden'); + $('#toc .title small').toggle(); + }, function() { + $('#toc .top').slideDown('fast'); + $('#toc').toggleClass('hidden'); + $('#toc .title small').toggle(); + }); +} + +function navResizeFn(e) { + if (e.which !== 1) { + navResizeFnStop(); + return; + } + + sessionStorage.navWidth = e.pageX.toString(); + $('.nav_wrap').css('width', e.pageX); + $('.nav_wrap').css('-ms-flex', 'inherit'); +} + +function navResizeFnStop() { + $(window).unbind('mousemove', navResizeFn); + window.removeEventListener('message', navMessageFn, false); +} + +function navMessageFn(e) { + if (e.data.action === 'mousemove') navResizeFn(e.data.event); + if (e.data.action === 'mouseup') navResizeFnStop(); +} + +function navResizer() { + $('#resizer').mousedown(function(e) { + e.preventDefault(); + $(window).mousemove(navResizeFn); + window.addEventListener('message', navMessageFn, false); + }); + $(window).mouseup(navResizeFnStop); + + if (sessionStorage.navWidth) { + navResizeFn({which: 1, pageX: parseInt(sessionStorage.navWidth, 10)}); + } +} + +function navExpander() { + var done = false, timer = setTimeout(postMessage, 500); + function postMessage() { + if (done) return; + clearTimeout(timer); + var opts = { action: 'expand', path: pathId }; + document.getElementById('nav').contentWindow.postMessage(opts, '*'); + done = true; + } + + window.addEventListener('message', function(event) { + if (event.data === 'navReady') postMessage(); + return false; + }, false); +} + +function mainFocus() { + var hash = window.location.hash; + if (hash !== '' && $(hash)[0]) { + $(hash)[0].scrollIntoView(); + } + + setTimeout(function() { $('#main').focus(); }, 10); +} + +function navigationChange() { + // This works around the broken anchor navigation with the YARD template. + window.onpopstate = function() { + var hash = window.location.hash; + if (hash !== '' && $(hash)[0]) { + $(hash)[0].scrollIntoView(); + } + }; +} + +$(document).ready(function() { + navResizer(); + navExpander(); + createSourceLinks(); + createDefineLinks(); + createFullTreeLinks(); + searchFrameButtons(); + linkSummaries(); + summaryToggle(); + constantSummaryToggle(); + generateTOC(); + mainFocus(); + navigationChange(); +}); + +})(); diff --git a/docs/source/_static/api-reference/js/full_list.js b/docs/source/_static/api-reference/js/full_list.js new file mode 100644 index 00000000..59069c5e --- /dev/null +++ b/docs/source/_static/api-reference/js/full_list.js @@ -0,0 +1,216 @@ +(function() { + +var $clicked = $(null); +var searchTimeout = null; +var searchCache = []; +var caseSensitiveMatch = false; +var ignoreKeyCodeMin = 8; +var ignoreKeyCodeMax = 46; +var commandKey = 91; + +RegExp.escape = function(text) { + return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); +} + +function escapeShortcut() { + $(document).keydown(function(evt) { + if (evt.which == 27) { + window.parent.postMessage('navEscape', '*'); + } + }); +} + +function navResizer() { + $(window).mousemove(function(e) { + window.parent.postMessage({ + action: 'mousemove', event: {pageX: e.pageX, which: e.which} + }, '*'); + }).mouseup(function(e) { + window.parent.postMessage({action: 'mouseup'}, '*'); + }); + window.parent.postMessage("navReady", "*"); +} + +function clearSearchTimeout() { + clearTimeout(searchTimeout); + searchTimeout = null; +} + +function enableLinks() { + // load the target page in the parent window + $('#full_list li').on('click', function(evt) { + $('#full_list li').removeClass('clicked'); + $clicked = $(this); + $clicked.addClass('clicked'); + evt.stopPropagation(); + + if (evt.target.tagName === 'A') return true; + + var elem = $clicked.find('> .item .object_link a')[0]; + var e = evt.originalEvent; + var newEvent = new MouseEvent(evt.originalEvent.type); + newEvent.initMouseEvent(e.type, e.canBubble, e.cancelable, e.view, e.detail, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, e.button, e.relatedTarget); + elem.dispatchEvent(newEvent); + evt.preventDefault(); + return false; + }); +} + +function enableToggles() { + // show/hide nested classes on toggle click + $('#full_list a.toggle').on('click', function(evt) { + evt.stopPropagation(); + evt.preventDefault(); + $(this).parent().parent().toggleClass('collapsed'); + highlight(); + }); +} + +function populateSearchCache() { + $('#full_list li .item').each(function() { + var $node = $(this); + var $link = $node.find('.object_link a'); + if ($link.length > 0) { + searchCache.push({ + node: $node, + link: $link, + name: $link.text(), + fullName: $link.attr('title').split(' ')[0] + }); + } + }); +} + +function enableSearch() { + $('#search input').keyup(function(event) { + if (ignoredKeyPress(event)) return; + if (this.value === "") { + clearSearch(); + } else { + performSearch(this.value); + } + }); + + $('#full_list').after(""); +} + +function ignoredKeyPress(event) { + if ( + (event.keyCode > ignoreKeyCodeMin && event.keyCode < ignoreKeyCodeMax) || + (event.keyCode == commandKey) + ) { + return true; + } else { + return false; + } +} + +function clearSearch() { + clearSearchTimeout(); + $('#full_list .found').removeClass('found').each(function() { + var $link = $(this).find('.object_link a'); + $link.text($link.text()); + }); + $('#full_list, #content').removeClass('insearch'); + $clicked.parents().removeClass('collapsed'); + highlight(); +} + +function performSearch(searchString) { + clearSearchTimeout(); + $('#full_list, #content').addClass('insearch'); + $('#noresults').text('').hide(); + partialSearch(searchString, 0); +} + +function partialSearch(searchString, offset) { + var lastRowClass = ''; + var i = null; + for (i = offset; i < Math.min(offset + 50, searchCache.length); i++) { + var item = searchCache[i]; + var searchName = (searchString.indexOf('::') != -1 ? item.fullName : item.name); + var matchString = buildMatchString(searchString); + var matchRegexp = new RegExp(matchString, caseSensitiveMatch ? "" : "i"); + if (searchName.match(matchRegexp) == null) { + item.node.removeClass('found'); + item.link.text(item.link.text()); + } + else { + item.node.addClass('found'); + item.node.removeClass(lastRowClass).addClass(lastRowClass == 'r1' ? 'r2' : 'r1'); + lastRowClass = item.node.hasClass('r1') ? 'r1' : 'r2'; + item.link.html(item.name.replace(matchRegexp, "$&")); + } + } + if(i == searchCache.length) { + searchDone(); + } else { + searchTimeout = setTimeout(function() { + partialSearch(searchString, i); + }, 0); + } +} + +function searchDone() { + searchTimeout = null; + highlight(); + if ($('#full_list li:visible').size() === 0) { + $('#noresults').text('No results were found.').hide().fadeIn(); + } else { + $('#noresults').text('').hide(); + } + $('#content').removeClass('insearch'); +} + +function buildMatchString(searchString, event) { + caseSensitiveMatch = searchString.match(/[A-Z]/) != null; + var regexSearchString = RegExp.escape(searchString); + if (caseSensitiveMatch) { + regexSearchString += "|" + + $.map(searchString.split(''), function(e) { return RegExp.escape(e); }). + join('.+?'); + } + return regexSearchString; +} + +function highlight() { + $('#full_list li:visible').each(function(n) { + $(this).removeClass('even odd').addClass(n % 2 == 0 ? 'odd' : 'even'); + }); +} + +/** + * Expands the tree to the target element and its immediate + * children. + */ +function expandTo(path) { + var $target = $(document.getElementById('object_' + path)); + $target.addClass('clicked'); + $target.removeClass('collapsed'); + $target.parentsUntil('#full_list', 'li').removeClass('collapsed'); + if($target[0]) { + window.scrollTo(window.scrollX, $target.offset().top - 250); + highlight(); + } +} + +function windowEvents(event) { + var msg = event.data; + if (msg.action === "expand") { + expandTo(msg.path); + } + return false; +} + +window.addEventListener("message", windowEvents, false); + +$(document).ready(function() { + escapeShortcut(); + navResizer(); + enableLinks(); + enableToggles(); + populateSearchCache(); + enableSearch(); +}); + +})(); diff --git a/docs/source/_static/api-reference/js/jquery.js b/docs/source/_static/api-reference/js/jquery.js new file mode 100644 index 00000000..198b3ff0 --- /dev/null +++ b/docs/source/_static/api-reference/js/jquery.js @@ -0,0 +1,4 @@ +/*! jQuery v1.7.1 jquery.com | jquery.org/license */ +(function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cv(a){if(!ck[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){cl||(cl=c.createElement("iframe"),cl.frameBorder=cl.width=cl.height=0),b.appendChild(cl);if(!cm||!cl.createElement)cm=(cl.contentWindow||cl.contentDocument).document,cm.write((c.compatMode==="CSS1Compat"?"":"")+""),cm.close();d=cm.createElement(a),cm.body.appendChild(d),e=f.css(d,"display"),b.removeChild(cl)}ck[a]=e}return ck[a]}function cu(a,b){var c={};f.each(cq.concat.apply([],cq.slice(0,b)),function(){c[this]=a});return c}function ct(){cr=b}function cs(){setTimeout(ct,0);return cr=f.now()}function cj(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ci(){try{return new a.XMLHttpRequest}catch(b){}}function cc(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g0){if(c!=="border")for(;g=0===c})}function S(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function K(){return!0}function J(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?parseFloat(d):j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=/-([a-z]|[0-9])/ig,w=/^-ms-/,x=function(a,b){return(b+"").toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=m.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7.1",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.add(a);return this},eq:function(a){a=+a;return a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;A.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").off("ready")}},bindReady:function(){if(!A){A=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||D.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw new Error(a)},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,"ms-").replace(v,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c
    a",d=q.getElementsByTagName("*"),e=q.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=q.getElementsByTagName("input")[0],b={leadingWhitespace:q.firstChild.nodeType===3,tbody:!q.getElementsByTagName("tbody").length,htmlSerialize:!!q.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:q.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete q.test}catch(s){b.deleteExpando=!1}!q.addEventListener&&q.attachEvent&&q.fireEvent&&(q.attachEvent("onclick",function(){b.noCloneEvent=!1}),q.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),q.appendChild(i),k=c.createDocumentFragment(),k.appendChild(q.lastChild),b.checkClone=k.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,k.removeChild(i),k.appendChild(q),q.innerHTML="",a.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",q.style.width="2px",q.appendChild(j),b.reliableMarginRight=(parseInt((a.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0);if(q.attachEvent)for(o in{submit:1,change:1,focusin:1})n="on"+o,p=n in q,p||(q.setAttribute(n,"return;"),p=typeof q[n]=="function"),b[o+"Bubbles"]=p;k.removeChild(q),k=g=h=j=q=i=null,f(function(){var a,d,e,g,h,i,j,k,m,n,o,r=c.getElementsByTagName("body")[0];!r||(j=1,k="position:absolute;top:0;left:0;width:1px;height:1px;margin:0;",m="visibility:hidden;border:0;",n="style='"+k+"border:5px solid #000;padding:0;'",o="
    "+""+"
    ",a=c.createElement("div"),a.style.cssText=m+"width:0;height:0;position:static;top:0;margin-top:"+j+"px",r.insertBefore(a,r.firstChild),q=c.createElement("div"),a.appendChild(q),q.innerHTML="
    t
    ",l=q.getElementsByTagName("td"),p=l[0].offsetHeight===0,l[0].style.display="",l[1].style.display="none",b.reliableHiddenOffsets=p&&l[0].offsetHeight===0,q.innerHTML="",q.style.width=q.style.paddingLeft="1px",f.boxModel=b.boxModel=q.offsetWidth===2,typeof q.style.zoom!="undefined"&&(q.style.display="inline",q.style.zoom=1,b.inlineBlockNeedsLayout=q.offsetWidth===2,q.style.display="",q.innerHTML="
    ",b.shrinkWrapBlocks=q.offsetWidth!==2),q.style.cssText=k+m,q.innerHTML=o,d=q.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,i={doesNotAddBorder:e.offsetTop!==5,doesAddBorderForTableAndCells:h.offsetTop===5},e.style.position="fixed",e.style.top="20px",i.fixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",i.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,i.doesNotIncludeMarginInBodyOffset=r.offsetTop!==j,r.removeChild(a),q=a=null,f.extend(b,i))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.nodeName.toLowerCase()]||f.valHooks[g.type];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;h=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/\bhover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function(a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")}; +f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;le&&i.push({elem:this,matches:d.slice(e)});for(j=0;j0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

    ";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
    ";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h0)for(h=g;h=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/",""],legend:[1,"
    ","
    "],thead:[1,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],col:[2,"","
    "],area:[1,"",""],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div
    ","
    "]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function() +{for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1>");try{for(var c=0,d=this.length;c1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||!bc.test("<"+a.nodeName)?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!_.test(k))k=b.createTextNode(k);else{k=k.replace(Y,"<$1>");var l=(Z.exec(k)||["",""])[1].toLowerCase(),m=bg[l]||bg._default,n=m[0],o=b.createElement("div");b===c?bh.appendChild(o):U(b).appendChild(o),o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=$.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]===""&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&X.test(k)&&o.insertBefore(b.createTextNode(X.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return br.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bq,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bq.test(g)?g.replace(bq,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bz(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bA=function(a,b){var c,d,e;b=b.replace(bs,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b)));return c}),c.documentElement.currentStyle&&(bB=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f===null&&g&&(e=g[b])&&(f=e),!bt.test(f)&&bu.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f||0,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),bz=bA||bB,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bD=/%20/g,bE=/\[\]$/,bF=/\r?\n/g,bG=/#.*$/,bH=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bI=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bJ=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bK=/^(?:GET|HEAD)$/,bL=/^\/\//,bM=/\?/,bN=/)<[^<]*)*<\/script>/gi,bO=/^(?:select|textarea)/i,bP=/\s+/,bQ=/([?&])_=[^&]*/,bR=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bS=f.fn.load,bT={},bU={},bV,bW,bX=["*/"]+["*"];try{bV=e.href}catch(bY){bV=c.createElement("a"),bV.href="",bV=bV.href}bW=bR.exec(bV.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bS)return bS.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
    ").append(c.replace(bN,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bO.test(this.nodeName)||bI.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bF,"\r\n")}}):{name:b.name,value:c.replace(bF,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b_(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b_(a,b);return a},ajaxSettings:{url:bV,isLocal:bJ.test(bW[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bX},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bZ(bT),ajaxTransport:bZ(bU),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?cb(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cc(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bH.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bG,"").replace(bL,bW[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bP),d.crossDomain==null&&(r=bR.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bW[1]&&r[2]==bW[2]&&(r[3]||(r[1]==="http:"?80:443))==(bW[3]||(bW[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),b$(bT,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bK.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bM.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bQ,"$1_="+x);d.url=y+(y===d.url?(bM.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bX+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=b$(bU,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)ca(g,a[g],c,e);return d.join("&").replace(bD,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cd=f.now(),ce=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cd++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(ce.test(b.url)||e&&ce.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(ce,l),b.url===j&&(e&&(k=k.replace(ce,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var cf=a.ActiveXObject?function(){for(var a in ch)ch[a](0,1)}:!1,cg=0,ch;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ci()||cj()}:ci,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,cf&&delete ch[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cg,cf&&(ch||(ch={},f(a).unload(cf)),ch[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var ck={},cl,cm,cn=/^(?:toggle|show|hide)$/,co=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cp,cq=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cr;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cu("show",3),a,b,c);for(var g=0,h=this.length;g=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cy(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cy(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,d,"padding")):this[d]():null},f.fn["outer"+c]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,d,a?"margin":"border")):this[d]():null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c],h=e.document.body;return e.document.compatMode==="CSS1Compat"&&g||h&&h["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var i=f.css(e,d),j=parseFloat(i);return f.isNumeric(j)?j:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window); \ No newline at end of file diff --git a/docs/source/_static/api-reference/method_list.html b/docs/source/_static/api-reference/method_list.html new file mode 100644 index 00000000..d17f26aa --- /dev/null +++ b/docs/source/_static/api-reference/method_list.html @@ -0,0 +1,715 @@ + + + + + + + + + + + + + + + + + + Method List + + + +
    +
    +

    Method List

    + + + +
    + + +
    + + diff --git a/docs/source/_static/api-reference/top-level-namespace.html b/docs/source/_static/api-reference/top-level-namespace.html new file mode 100644 index 00000000..285cd5df --- /dev/null +++ b/docs/source/_static/api-reference/top-level-namespace.html @@ -0,0 +1,126 @@ + + + + + + + Top Level Namespace + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
    + + +

    Top Level Namespace + + + +

    +
    + + + + + + +
    +
    Includes:
    +
    MultiTenantFindBy
    +
    + + + + + + +
    + +

    Defined Under Namespace

    +

    + + + Modules: ActiveRecord, MultiTenant, MultiTenantFindBy, Sidekiq + + + + +

    + + + + + + + + + + + + + + +

    Method Summary

    + +

    Methods included from MultiTenantFindBy

    +

    #cached_find_by_statement

    + + +
    + + + +
    + + \ No newline at end of file diff --git a/docs/source/_templates/.gitignore b/docs/source/_templates/.gitignore new file mode 100644 index 00000000..5e7d2734 --- /dev/null +++ b/docs/source/_templates/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/docs/source/api-reference.rst b/docs/source/api-reference.rst new file mode 100644 index 00000000..917ed131 --- /dev/null +++ b/docs/source/api-reference.rst @@ -0,0 +1,8 @@ +.. _api-reference: + +API Reference +============= + +This section provides a detailed overview of the available classes, modules, and methods in the ``activerecord-multi-tenant`` gem. + +`Click here to open the HTML file <_static/api-reference/index.html>`_ \ No newline at end of file diff --git a/docs/source/appendix.rst b/docs/source/appendix.rst new file mode 100644 index 00000000..4a538d3a --- /dev/null +++ b/docs/source/appendix.rst @@ -0,0 +1,26 @@ +.. _appendix: + +Appendix +======== + +This section provides additional resources and acknowledgments related to ``activerecord-multi-tenant``. + +Glossary of Terms and Abbreviations +----------------------------------- + +- **Multi-tenancy:** A software architecture in which a single instance of software serves multiple tenants. +- **Tenant:** A group of users who share a common access with specific privileges to the software instance. +- **Tenant ID:** A unique identifier for a tenant. + + +References to External Resources +-------------------------------- + +- `Official Rails Guide `_: A comprehensive guide to Ruby on Rails. +- `ActiveRecord Documentation `_: Detailed documentation for ActiveRecord, the database toolkit used by ``activerecord-multi-tenant``. + +Acknowledgments and Credits +--------------------------- + +We would like to thank the Ruby on Rails community for their contributions to open source, which have made projects like ``activerecord-multi-tenant`` possible. +This gem was initially based on `acts_as_tenant `, and still shares some code. We thank the authors for their efforts. diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst new file mode 100644 index 00000000..af3bc4ba --- /dev/null +++ b/docs/source/changelog.rst @@ -0,0 +1,8 @@ +. _changelog: + +Changelog +========= + +This section provides a history of changes for each version of ``activerecord-multi-tenant``. +For a complete history of changes, please refer to the official changelog on `GitHub _`. + diff --git a/docs/source/community-and-support.rst b/docs/source/community-and-support.rst new file mode 100644 index 00000000..a28d42e1 --- /dev/null +++ b/docs/source/community-and-support.rst @@ -0,0 +1,26 @@ +.. _community-and-support: + +Community and Support +===================== + +If you need help with ``activerecord-multi-tenant``, there are several resources available to you. + +GitHub Repository +----------------- + +The `activerecord-multi-tenant GitHub repository `_ is the first place to look for code, issues, and pull requests. + +Issue Tracker +------------- + +If you encounter issues with ``activerecord-multi-tenant``, you can report them in the `issue tracker `_. Please provide as much detail as possible so we can address the issue effectively. + +Discussion Forums +----------------- + +For general discussions about ``activerecord-multi-tenant``, you can use the `discussion forums `_. This is a great place to ask questions, share ideas, and engage with the ``activerecord-multi-tenant`` community. + +Documentation Feedback +---------------------- + +We strive to provide high-quality documentation for ``activerecord-multi-tenant``. If you have feedback or suggestions for improving the documentation, please open an issue in the `issue tracker `_. diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 00000000..5d01cfb0 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,30 @@ +from datetime import date + +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = "ActiveRecord Multi-tenant" +copyright = f"{date.today().year} .Citus Data Licensed under the MIT license, see License for details. " +author = "Citus Data" +release = "2.2.0" + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = ["sphinxnotes.strike"] + +templates_path = ["_templates"] +exclude_patterns = [] + +language = "python" + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "sphinx_rtd_theme" +html_static_path = ["_static"] diff --git a/docs/source/contributing.rst b/docs/source/contributing.rst new file mode 100644 index 00000000..a50a6608 --- /dev/null +++ b/docs/source/contributing.rst @@ -0,0 +1,70 @@ +.. _contributing: + +Contributing +============ + +We welcome contributions to ``activerecord-multi-tenant``! This section provides guidelines for contributing to the project. + +Overview of the Development Process +----------------------------------- + +``activerecord-multi-tenant`` is developed using a standard fork and pull request model. The `activerecord-multi-tenant GitHub repository `_ is the starting point for code contributions. + +Guidelines for Contributing +--------------------------- + +1. **Fork the Repository:** Start by forking the official ``activerecord-multi-tenant`` repository to your own GitHub account. + +2. **Clone the Repository:** Clone the forked repository to your local machine and add the official repository as an upstream remote: + + .. code-block:: bash + + $ git clone https://github.com/citusdata/activerecord-multi-tenant.git + $ cd activerecord-multi-tenant + $ git remote add upstream https://github.com/your-github-account/activerecord-multi-tenant.git + +3. **Create a Feature Branch:** Create a new branch for each feature or bugfix: + + .. code-block:: bash + + $ git checkout -b my-feature-branch + +4. **Commit Your Changes:** Make your changes and commit them to your feature branch. + +5. **Push to GitHub:** Push your changes to your fork on GitHub: + + .. code-block:: bash + + $ git push origin my-feature-branch + +6. **Submit a Pull Request:** Open a pull request from your feature branch to the master branch of the official ``activerecord-multi-tenant`` repository. + +Please ensure your code adheres to the existing style conventions of the project. Include tests for any new features or bug fixes, and update the documentation as necessary. + +Setting Up a Development Environment +------------------------------------ + +To set up a development environment for ``activerecord-multi-tenant``, follow these steps: + +1. Clone the repository as described in the contributing guidelines. + +2. Install the required dependencies: + + .. code-block:: bash + + $ bundle install + +3. Run the tests to ensure everything is set up correctly: + + .. code-block:: bash + + $ bundle exec rake spec + +4. Compile documentation for the project: + + .. code-block:: bash + + $ cd docs + $ make pre-build + $ make html + diff --git a/docs/source/getting-started.rst b/docs/source/getting-started.rst new file mode 100644 index 00000000..f6aeb123 --- /dev/null +++ b/docs/source/getting-started.rst @@ -0,0 +1,37 @@ +.. _getting-started: + +Getting Started +=============== + +This section will guide you through the process of installing and setting up ``activerecord-multi-tenant`` in your Rails application. + +Installation +------------ + +To install ``activerecord-multi-tenant``, add the following line to your application's Gemfile: + +.. code-block:: ruby + + gem install activerecord-multi-tenant + +Then execute: + +.. code-block:: bash + + $ bundle install + +Or install it yourself as: + +.. code-block:: bash + + $ gem install activerecord-multi-tenant + +Dependencies +------------ + +``activerecord-multi-tenant`` requires: + +- Ruby version 3.0.0 or later +- Rails version 6.0.0 or later + +Please ensure that your application meets these requirements before installing the gem. diff --git a/docs/source/guides-and-tutorials.rst b/docs/source/guides-and-tutorials.rst new file mode 100644 index 00000000..74ba29ce --- /dev/null +++ b/docs/source/guides-and-tutorials.rst @@ -0,0 +1,129 @@ +.. _guides-and-tutorials: + +Guides and Tutorials +==================== + +This section provides step-by-step guides and tutorials on how to use ``activerecord-multi-tenant`` in various scenarios. + +Setting Up Multi tenancy in a New Project +------------------------------------------ + +To set up multi-tenancy in a new Rails project, follow these steps: + +1. Install the ``activerecord-multi-tenant`` gem as described in the :ref:`getting-started` section. + +2. Declare your tenant model in your ActiveRecord models: + + .. code-block:: ruby + + class User < ActiveRecord::Base + multi_tenant :company + end + +3. Set the current tenant before executing queries: + + .. code-block:: ruby + + ActiveRecord::MultiTenant.current_tenant = Company.first + @users = User.all + +Migrating an Existing Project to Use ``activerecord-multi-tenant`` +------------------------------------------------------------------ + +If you have an existing Rails project and you want to add multi-tenancy support, follow these steps: + +1. Install the ``activerecord-multi-tenant`` gem as described in the :ref:`getting-started` section. + +2. Update your ActiveRecord models to declare the tenant model: + + .. code-block:: ruby + + class User < ActiveRecord::Base + multi_tenant :company + end + +3. Update your application logic to set the current tenant before executing queries. + + +Using ``has_many`` , ``has_one`` , and ``belongs_to`` Associations +------------------------------------------------------------------ + +When using ``has_many``, ``has_one``, and ``belongs_to`` associations, +there is nothing special you need to do to make them work with ``activerecord-multi-tenant``. +The gem will automatically scope the associations to the current tenant.: + +.. code-block:: ruby + + class User < ActiveRecord::Base + multi_tenant :company + has_many :posts + end + + class Post < ActiveRecord::Base + belongs_to :user + end + + ActiveRecord::MultiTenant.with(Company.first) do + @user = User.first + @user.posts # => Returns posts belonging to Company.first + end + +Using ``has_and_belongs_to_many`` Associations +----------------------------------------------- + +When using ``has_and_belongs_to_many`` associations, you need to specify the tenant column and tenant class name to +scope the association to the current tenant. If you set the ``tenant_enabled`` option to ``false``, the gem will +not scope the association to the current tenant. + +.. code-block:: ruby + + class Account < ActiveRecord::Base + multi_tenant :account + has_many :projects + has_one :manager, inverse_of: :account + has_many :optional_sub_tasks + end + + class Manager < ActiveRecord::Base + multi_tenant :account + belongs_to :project + has_and_belongs_to_many :tasks, { tenant_column: :account_id, tenant_enabled: true, + tenant_class_name: 'Account' } + end + + # Tests to check if the tenant column is set correctly + let(:task1) { Task.create! name: 'task1', project: project1, account: account1 } + let(:manager1) { Manager.create! name: 'manager1', account: account1, tasks: [task1] } + + MultiTenant.with(account1) do + expect(manager1.tasks.first.account_id).to eq(task1.account_id) # true + end + +Using ``activerecord-multi-tenant`` with Controllers +----------------------------------------------------- + +When using ``activerecord-multi-tenant`` with controllers, you need to set the current tenant in the controller +before executing queries. You can do this by overriding the ``set_current_tenant`` method in your controller: + +.. code-block:: ruby + + class ApplicationController < ActionController::Base + set_current_tenant_through_filter # Required to opt into this behavior + before_action :set_customer_as_tenant + + def set_customer_as_tenant + customer = Customer.find(session[:current_customer_id]) + set_current_tenant(customer) + end + end + +Best Practices and Recommendations +----------------------------------- + +When using ``activerecord-multi-tenant``, keep the following best practices in mind: + +- Always set the current tenant before executing queries in a multitenant context. +- Be mindful of the tenant scope when writing complex queries or joins. +- If you prefer not to set a tenant for the global context, but need to specify one for certain sections of code, + you can utilize the `MultiTenant.with(tenant)` function. This will assign the `tenant` value + to the specific code block where it's used. diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 00000000..67c1e5cc --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,51 @@ +.. Django Multi-tenant documentation master file, created by + sphinx-quickstart on Mon Feb 13 13:32:28 2023. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to ActiveRecord Multi-tenant's documentation! +====================================================== + +[TODO: Change documentation link to actual documentation link] + +|Latest Documentation Status| |Build Status| |Coverage Status| |RubyGems Version| + +.. |Latest Documentation Status| image:: https://readthedocs.org/projects/django-multitenant/badge/?version=latest + :target: https://activerecord-multi-tenant.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status + +.. |Build Status| image:: https://github.com/citusdata/activerecord-multi-tenant/actions/workflows/active-record-multi-tenant-tests.yml/badge.svg + :target: https://github.com/citusdata/activerecord-multi-tenant/actions/workflows/active-record-multi-tenant-tests.yml + :alt: Build Status + +.. |Coverage Status| image:: https://codecov.io/gh/citusdata/activerecord-multi-tenant/branch/master/graph/badge.svg?token=rw0TsEk4Ld + :target: https://codecov.io/gh/citusdata/activerecord-multi-tenant + :alt: Coverage Status + +.. |RubyGems Version| image:: https://badge.fury.io/rb/activerecord-multi-tenant.svg + :target: https://badge.fury.io/rb/activerecord-multi-tenant + +activerecord-multi-tenant Documentation +======================================== + +Welcome to the official documentation for ``activerecord-multi-tenant``, a powerful and flexible gem for adding multi-tenancy support to your Rails applications using ActiveRecord. + +.. toctree:: + :maxdepth: 2 + :caption: Table of Contents + + introduction + getting-started + usage-guide + guides-and-tutorials + api-reference + troubleshooting + contributing + changelog + community-and-support + appendix + license + +This documentation provides a comprehensive guide to using ``activerecord-multi-tenant``, including installation and configuration instructions, usage examples, API reference, troubleshooting tips, and more. + +Whether you're new to multi-tenancy or an experienced developer looking to integrate ``activerecord-multi-tenant`` into your existing Rails application, this documentation aims to provide the information you need. diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst new file mode 100644 index 00000000..e76d15e3 --- /dev/null +++ b/docs/source/introduction.rst @@ -0,0 +1,33 @@ +.. _introduction: + +Introduction +============ + +Welcome to the official documentation for ``activerecord-multi-tenant``, a powerful and flexible gem for adding multi-tenancy support to your Rails applications using ActiveRecord. + +Overview +-------- + +``activerecord-multi-tenant`` is designed to help developers manage and interact with data in multi-tenant environments. It provides a simple and intuitive API to scope your ActiveRecord models to specific tenants, ensuring data isolation and security while maintaining the simplicity and elegance that Rails developers love. + +Purpose +------- + +The purpose of this documentation is to provide a comprehensive guide to using ``activerecord-multi-tenant``. Whether you're new to multi-tenancy or an experienced developer looking to integrate ``activerecord-multi-tenant`` into your existing Rails application, this documentation aims to provide the information you need. + +Benefits of Using ``activerecord-multi-tenant`` +------------------------------------------------ + +With ``activerecord-multi-tenant``, you can: + +- Easily scope your ActiveRecord models to specific tenants +- Maintain data isolation and security in multi-tenant environments +- Seamlessly integrate with your existing Rails applications +- Benefit from the simplicity and elegance of the Rails framework + +Target Audience +--------------- + +This documentation is intended for developers who are familiar with Ruby on Rails and ActiveRecord. Knowledge of multi-tenancy concepts is beneficial but not required, as we will cover these topics in the following sections. + +We hope you find this documentation helpful as you explore the capabilities of ``activerecord-multi-tenant``. Let's get started! diff --git a/docs/source/license.rst b/docs/source/license.rst new file mode 100644 index 00000000..3258400a --- /dev/null +++ b/docs/source/license.rst @@ -0,0 +1,22 @@ +.. _license: + +License +=============================================== +Copyright (c) 2023 , Citus Data Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/docs/source/troubleshooting.rst b/docs/source/troubleshooting.rst new file mode 100644 index 00000000..3fb791dc --- /dev/null +++ b/docs/source/troubleshooting.rst @@ -0,0 +1,41 @@ +.. _troubleshooting: + +Troubleshooting +=============== + +This section provides solutions to common issues you might encounter when using ``activerecord-multi-tenant``. + +Common Issues and Their Solutions +--------------------------------- + +**Issue:** Tenant scope is not applied to queries. + +**Solution:** Make sure you've set the current tenant before executing queries. Use the MultiTenant.with method to set the current tenant. For example: + +.. code-block:: ruby + + MultiTenant.with(customer) do + site = Site.find(params[:site_id]) + site.update! last_accessed_at: Time.now + site.page_views.count + end + + + +FAQs and Known Limitations +-------------------------- + +**Q: Can I use multiple tenant models in the same application?** + +**A:** Yes, you can declare different tenant models in different ActiveRecord models. However, you can only set one current tenant at a time. + +**Q: Does ``activerecord-multi-tenant`` support Rails version 5.X?** + +**A:** ``activerecord-multi-tenant`` supports Rails 6.0.0 and later. For older versions of Rails, please use the appropriate version of the gem. + + + +Reporting Bugs and Requesting Features +-------------------------------------- + +If you encounter a bug or have a feature request, please open an issue on the `activerecord-multi-tenant GitHub repository `_. Please provide as much detail as possible so we can address the issue effectively. diff --git a/docs/source/usage-guide.rst b/docs/source/usage-guide.rst new file mode 100644 index 00000000..7025a73f --- /dev/null +++ b/docs/source/usage-guide.rst @@ -0,0 +1,59 @@ +.. _usage-guide: + +Usage Guide +=========== + +This section provides a comprehensive guide on how to use ``activerecord-multi-tenant`` in your Rails application. + +Basic Usage +----------- + +To use ``activerecord-multi-tenant``, you need to declare the tenant model in your ActiveRecord models. Here's an example: + +.. code-block:: ruby + + class PageView < ActiveRecord::Base + multi_tenant :customer + belongs_to :site + + # ... + end + + class Site < ActiveRecord::Base + multi_tenant :customer + has_many :page_views + + # ... + end + +In this example, the ``PageView`` and ``Site`` models are scoped to the ``Customer`` model. This means that each user belongs to a specific customer. + + +Then wrap all code that runs queries/modifications in blocks like this: + +.. code-block:: ruby + + customer = Customer.find(session[:current_customer_id]) + # ... + MultiTenant.with(customer) do + site = Site.find(params[:site_id]) + site.update! last_accessed_at: Time.now + site.page_views.count + end + +Alternatively, if you don't want to use a block, you can set the current tenant explicitly: + +.. code-block:: ruby + + customer = Customer.find(session[:current_customer_id]) + MultiTenant.current_tenant = customer + + +Multi-tenancy Concepts and Terminology +-------------------------------------- + +Multi-tenancy is a software architecture in which a single instance of software serves multiple tenants. A tenant is a group of users who share a common access with specific privileges to the software instance. + +In the context of ``activerecord-multi-tenant``, a tenant is typically represented by a model in your Rails application (e.g., ``Customer``), and other models (e.g., ``PageView``) are scoped to this tenant model. + + diff --git a/spec/activerecord-multi-tenant/associations_spec.rb b/spec/activerecord-multi-tenant/associations_spec.rb index 248546b5..731e7b58 100644 --- a/spec/activerecord-multi-tenant/associations_spec.rb +++ b/spec/activerecord-multi-tenant/associations_spec.rb @@ -23,13 +23,13 @@ it 'check has_many_belongs_to' do MultiTenant.with(account1) do - expect(manager1.tasks.first.account_id).to eq(task1.id) # has_many + expect(manager1.tasks.first.account_id).to eq(task1.account_id) # has_many end end it 'check has_many_belongs_to without tenant in the intermediate table' do MultiTenant.with(account1) do - expect(manager1.tasks.first.account_id).to eq(task1.id) # has_many + expect(manager1.tasks.first.account_id).to eq(task1.account_id) # has_many end end From fa0e2b36d7ffcad846f8c811e5309cbc232d38af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCrkan=20=C4=B0ndibay?= Date: Sun, 28 May 2023 06:47:01 +0300 Subject: [PATCH 084/105] Organizes badges in documentation and README.md (#197) --- README.md | 2 +- docs/source/index.rst | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3d589cea..a3320e8c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# activerecord-multi-tenant [ ![](https://img.shields.io/gem/v/activerecord-multi-tenant.svg)](https://rubygems.org/gems/activerecord-multi-tenant) [ ![](https://img.shields.io/gem/dt/activerecord-multi-tenant.svg)](https://rubygems.org/gems/activerecord-multi-tenant) +# activerecord-multi-tenant [![Build Status](https://github.com/citusdata/activerecord-multi-tenant/actions/workflows/active-record-multi-tenant-tests.yml/badge.svg)](https://github.com/citusdata/activerecord-multi-tenant/actions/workflows/active-record-multi-tenant-tests.yml) [![codecov](https://codecov.io/gh/citusdata/activerecord-multi-tenant/branch/master/graph/badge.svg?token=rw0TsEk4Ld)](https://codecov.io/gh/citusdata/activerecord-multi-tenant) [ ![Gems Version](https://img.shields.io/gem/v/activerecord-multi-tenant.svg)](https://rubygems.org/gems/activerecord-multi-tenant)[ ![Gem Download Count](https://img.shields.io/gem/dt/activerecord-multi-tenant.svg)](https://rubygems.org/gems/activerecord-multi-tenant) [![Documentation Status](https://readthedocs.org/projects/activerecord-multi-tenant/badge/?version=latest)](https://activerecord-multi-tenant.readthedocs.io/en/latest/?badge=latest) Introduction Post: https://www.citusdata.com/blog/2017/01/05/easily-scale-out-multi-tenant-apps/ diff --git a/docs/source/index.rst b/docs/source/index.rst index 67c1e5cc..a98d9281 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -10,9 +10,6 @@ Welcome to ActiveRecord Multi-tenant's documentation! |Latest Documentation Status| |Build Status| |Coverage Status| |RubyGems Version| -.. |Latest Documentation Status| image:: https://readthedocs.org/projects/django-multitenant/badge/?version=latest - :target: https://activerecord-multi-tenant.readthedocs.io/en/latest/?badge=latest - :alt: Documentation Status .. |Build Status| image:: https://github.com/citusdata/activerecord-multi-tenant/actions/workflows/active-record-multi-tenant-tests.yml/badge.svg :target: https://github.com/citusdata/activerecord-multi-tenant/actions/workflows/active-record-multi-tenant-tests.yml @@ -22,8 +19,17 @@ Welcome to ActiveRecord Multi-tenant's documentation! :target: https://codecov.io/gh/citusdata/activerecord-multi-tenant :alt: Coverage Status -.. |RubyGems Version| image:: https://badge.fury.io/rb/activerecord-multi-tenant.svg - :target: https://badge.fury.io/rb/activerecord-multi-tenant +.. |RubyGems Version| image:: https://img.shields.io/gem/v/activerecord-multi-tenant.svg + :target: https://rubygems.org/gems/activerecord-multi-tenant + +.. |Gem Downloads| image:: https://img.shields.io/gem/dt/activerecord-multi-tenant.svg + :target: https://rubygems.org/gems/activerecord-multi-tenant + :alt: Gem Downloads + +.. |Latest Documentation Status| image:: https://readthedocs.org/projects/django-multitenant/badge/?version=latest + :target: https://activerecord-multi-tenant.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status + activerecord-multi-tenant Documentation ======================================== From e6a7d4cf690de1ff153b13e9148ce7ab18e7a81c Mon Sep 17 00:00:00 2001 From: gindibay Date: Sun, 28 May 2023 06:55:41 +0300 Subject: [PATCH 085/105] Changes order of badges in docs --- docs/source/index.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index a98d9281..6e5c26aa 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -6,10 +6,7 @@ Welcome to ActiveRecord Multi-tenant's documentation! ====================================================== -[TODO: Change documentation link to actual documentation link] - -|Latest Documentation Status| |Build Status| |Coverage Status| |RubyGems Version| - +|Build Status| |Coverage Status| |RubyGems Version| |Gem Downloads| |Latest Documentation Status| .. |Build Status| image:: https://github.com/citusdata/activerecord-multi-tenant/actions/workflows/active-record-multi-tenant-tests.yml/badge.svg :target: https://github.com/citusdata/activerecord-multi-tenant/actions/workflows/active-record-multi-tenant-tests.yml From 076db75a3a5faac42adb1e3933e43f22f86ba921 Mon Sep 17 00:00:00 2001 From: gindibay Date: Sun, 28 May 2023 10:45:15 +0300 Subject: [PATCH 086/105] Fixes url problem in documentation --- docs/source/changelog.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index af3bc4ba..3f3f3945 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -1,8 +1,8 @@ -. _changelog: +.. _changelog: Changelog ========= This section provides a history of changes for each version of ``activerecord-multi-tenant``. -For a complete history of changes, please refer to the official changelog on `GitHub _`. +For a complete history of changes, please refer to the official changelog on `GitHub `_. From 16fd8518c81f6e8e515d658495db469de4e20313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCrkan=20=C4=B0ndibay?= Date: Fri, 2 Jun 2023 16:51:24 +0300 Subject: [PATCH 087/105] Aligns badges --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a3320e8c..34e97d8b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# activerecord-multi-tenant [![Build Status](https://github.com/citusdata/activerecord-multi-tenant/actions/workflows/active-record-multi-tenant-tests.yml/badge.svg)](https://github.com/citusdata/activerecord-multi-tenant/actions/workflows/active-record-multi-tenant-tests.yml) [![codecov](https://codecov.io/gh/citusdata/activerecord-multi-tenant/branch/master/graph/badge.svg?token=rw0TsEk4Ld)](https://codecov.io/gh/citusdata/activerecord-multi-tenant) [ ![Gems Version](https://img.shields.io/gem/v/activerecord-multi-tenant.svg)](https://rubygems.org/gems/activerecord-multi-tenant)[ ![Gem Download Count](https://img.shields.io/gem/dt/activerecord-multi-tenant.svg)](https://rubygems.org/gems/activerecord-multi-tenant) [![Documentation Status](https://readthedocs.org/projects/activerecord-multi-tenant/badge/?version=latest)](https://activerecord-multi-tenant.readthedocs.io/en/latest/?badge=latest) +# activerecord-multi-tenant +[![Build Status](https://github.com/citusdata/activerecord-multi-tenant/actions/workflows/active-record-multi-tenant-tests.yml/badge.svg)](https://github.com/citusdata/activerecord-multi-tenant/actions/workflows/active-record-multi-tenant-tests.yml) [![codecov](https://codecov.io/gh/citusdata/activerecord-multi-tenant/branch/master/graph/badge.svg?token=rw0TsEk4Ld)](https://codecov.io/gh/citusdata/activerecord-multi-tenant) [ ![Gems Version](https://img.shields.io/gem/v/activerecord-multi-tenant.svg)](https://rubygems.org/gems/activerecord-multi-tenant)[ ![Gem Download Count](https://img.shields.io/gem/dt/activerecord-multi-tenant.svg)](https://rubygems.org/gems/activerecord-multi-tenant) [![Documentation Status](https://readthedocs.org/projects/activerecord-multi-tenant/badge/?version=latest)](https://activerecord-multi-tenant.readthedocs.io/en/latest/?badge=latest) Introduction Post: https://www.citusdata.com/blog/2017/01/05/easily-scale-out-multi-tenant-apps/ From 7b6bb2fe90c63e8c9dc868b476dfb35160bf0685 Mon Sep 17 00:00:00 2001 From: Shojiro Yanagisawa <2967009+nipe0324@users.noreply.github.com> Date: Mon, 5 Jun 2023 14:34:35 +0900 Subject: [PATCH 088/105] Wrap ActiveRecord::Base with ActiveSupport.on_load (#199) * Wrap ActiveRecord::Base with ActiveSupport.on_load * Test inspect method filters senstive column values * Remove set secret_token and secret_key_base in spec_helper.rb * Comment reason of filter_parameters in spec_helper.rb --- .../query_rewriter.rb | 5 ++-- .../model_extensions_spec.rb | 7 ++++++ spec/schema.rb | 1 + spec/spec_helper.rb | 23 +++++++++++++------ 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/lib/activerecord-multi-tenant/query_rewriter.rb b/lib/activerecord-multi-tenant/query_rewriter.rb index da1f3d5d..c8bbca9e 100644 --- a/lib/activerecord-multi-tenant/query_rewriter.rb +++ b/lib/activerecord-multi-tenant/query_rewriter.rb @@ -370,7 +370,6 @@ def relations_from_node_join(node_join) end end -require 'active_record/base' module MultiTenantFindBy def cached_find_by_statement(key, &block) return super unless respond_to?(:scoped_by_tenant?) && scoped_by_tenant? @@ -380,4 +379,6 @@ def cached_find_by_statement(key, &block) end end -ActiveRecord::Base.singleton_class.prepend(MultiTenantFindBy) +ActiveSupport.on_load(:active_record) do |base| + base.singleton_class.prepend(MultiTenantFindBy) +end diff --git a/spec/activerecord-multi-tenant/model_extensions_spec.rb b/spec/activerecord-multi-tenant/model_extensions_spec.rb index 2ae94f8a..a49af989 100644 --- a/spec/activerecord-multi-tenant/model_extensions_spec.rb +++ b/spec/activerecord-multi-tenant/model_extensions_spec.rb @@ -131,6 +131,13 @@ def self.name it { expect(@posts).to eq([@post1]) } end + describe 'inspect method filters senstive column values' do + it 'filters senstive value' do + account = Account.new(name: 'foo', password: 'baz') + expect(account.inspect).to eq '#' + end + end + # Scoping models describe 'Project.all should be scoped to the current tenant if set' do before do diff --git a/spec/schema.rb b/spec/schema.rb index edc136cb..93c6fd74 100644 --- a/spec/schema.rb +++ b/spec/schema.rb @@ -9,6 +9,7 @@ t.column :name, :string t.column :subdomain, :string t.column :domain, :string + t.column :password, :string end create_table :projects, force: true, partition_key: :account_id do |t| diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9ea5c333..a64d41df 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -27,8 +27,24 @@ require 'action_controller/railtie' require 'rspec/rails' +module MultiTenantTest + class Application < Rails::Application; end +end + +# Specifies columns which shouldn't be exposed while calling #inspect. +ActiveSupport.on_load(:active_record) do + self.filter_attributes += MultiTenantTest::Application.config.filter_parameters +end + require 'activerecord_multi_tenant' +# It's necessary for testing the filtering of senstive column values in ActiveRecord. +# Refer to "describe 'inspect method filters senstive column values'" +# +# To verify that ActiveSupport.on_load(:active_record) is not being unnecessarily invoked, +# this line should be placed after "require 'activerecord_multi_tenant'" and before ActiveRecord::Base is called. +MultiTenantTest::Application.config.filter_parameters = [:password] + require 'bundler' Bundler.require(:default, :development) require_relative './support/format_sql' @@ -55,13 +71,6 @@ end end -module MultiTenantTest - class Application < Rails::Application; end -end - -MultiTenantTest::Application.config.secret_token = 'x' * 40 -MultiTenantTest::Application.config.secret_key_base = 'y' * 40 - # rubocop:disable Lint/UnusedMethodArgument # changing the name of the parameter breaks tests def with_belongs_to_required_by_default(&block) From fd42dbdf0aa5697fe44297c90e889be990157608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCrkan=20=C4=B0ndibay?= Date: Tue, 6 Jun 2023 10:39:21 +0300 Subject: [PATCH 089/105] Adds Changelog entries for 2.3.0 (#201) * Adds Changelog entries for 2.3.0 * Updates version into 2.3.0 in version file * Adds minimum coverage as 80 to be able to change version --- CHANGELOG.md | 6 ++++++ lib/activerecord-multi-tenant/version.rb | 2 +- spec/spec_helper.rb | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5f205c5..c445f1c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,10 @@ # Changelog +## 2.3.0 2023-06-05 +* Adds has_and_belongs_to_many feature with tenant (#193) +* Removes eol ruby versions +* Adds documentation in ReadTheDocs platform (#196) +* Organizes badges in documentation and README.md (#197) +* Wrap ActiveRecord::Base with ActiveSupport.on_load (#199) ## 2.2.0 2022-12-06 * Handle changing tenant from `nil` to a value [#173](https://github.com/citusdata/activerecord-multi-tenant/pull/173) diff --git a/lib/activerecord-multi-tenant/version.rb b/lib/activerecord-multi-tenant/version.rb index 480a0194..20a076ea 100644 --- a/lib/activerecord-multi-tenant/version.rb +++ b/lib/activerecord-multi-tenant/version.rb @@ -1,3 +1,3 @@ module MultiTenant - VERSION = '2.2.0'.freeze + VERSION = '2.3.0'.freeze end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a64d41df..2fdb9947 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -18,6 +18,7 @@ add_group 'Lib', '/lib' # Include the lib directory for coverage puts "Tracked files: #{SimpleCov.tracked_files}" end + SimpleCov.minimum_coverage 80 require 'simplecov-cobertura' SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter From bed29b1d859279a290a10f621210c2908300356b Mon Sep 17 00:00:00 2001 From: Sasaki Motoaki <61261492+msasaki666@users.noreply.github.com> Date: Fri, 16 Jun 2023 18:55:40 +0900 Subject: [PATCH 090/105] update logic inside the tenant_klass_defined? method (#202) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix logic inside the tenant_klass_defined method * add tests for tenant_klass_defined? * fix failed tests * rubocop -a * remove unnecessary word * add a test case for the following https://github.com/citusdata/activerecord-multi-tenant/issues/105 * test a more appropriate class * add multi_tenant * Fixes rubocop warnings --------- Co-authored-by: Gürkan İndibay --- .../model_extensions.rb | 4 +- lib/activerecord-multi-tenant/multi_tenant.rb | 9 +++- .../multi_tenant_spec.rb | 54 +++++++++++++++++++ 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/lib/activerecord-multi-tenant/model_extensions.rb b/lib/activerecord-multi-tenant/model_extensions.rb index 0ec84f98..20566519 100644 --- a/lib/activerecord-multi-tenant/model_extensions.rb +++ b/lib/activerecord-multi-tenant/model_extensions.rb @@ -67,7 +67,7 @@ def inherited(subclass) partition_key = @partition_key # Create an implicit belongs_to association only if tenant class exists - if MultiTenant.tenant_klass_defined?(tenant_name) + if MultiTenant.tenant_klass_defined?(tenant_name, options) belongs_to tenant_name, **options.slice(:class_name, :inverse_of, :optional) .merge(foreign_key: options[:partition_key]) end @@ -103,7 +103,7 @@ def inherited(subclass) tenant_id end - if MultiTenant.tenant_klass_defined?(tenant_name) + if MultiTenant.tenant_klass_defined?(tenant_name, options) define_method "#{tenant_name}=" do |model| super(model) if send("#{partition_key}_changed?") && persisted? && !send("#{partition_key}_was").nil? diff --git a/lib/activerecord-multi-tenant/multi_tenant.rb b/lib/activerecord-multi-tenant/multi_tenant.rb index b2edb592..b541e33a 100644 --- a/lib/activerecord-multi-tenant/multi_tenant.rb +++ b/lib/activerecord-multi-tenant/multi_tenant.rb @@ -5,8 +5,13 @@ class Current < ::ActiveSupport::CurrentAttributes attribute :tenant end - def self.tenant_klass_defined?(tenant_name) - !!tenant_name.to_s.classify.safe_constantize + def self.tenant_klass_defined?(tenant_name, options = {}) + class_name = if options[:class_name].present? + options[:class_name] + else + tenant_name.to_s.classify + end + !!class_name.safe_constantize end def self.partition_key(tenant_name) diff --git a/spec/activerecord-multi-tenant/multi_tenant_spec.rb b/spec/activerecord-multi-tenant/multi_tenant_spec.rb index 0cae7227..cedd0bbe 100644 --- a/spec/activerecord-multi-tenant/multi_tenant_spec.rb +++ b/spec/activerecord-multi-tenant/multi_tenant_spec.rb @@ -64,4 +64,58 @@ end end end + + describe '.tenant_klass_defined?' do + context 'without options' do + before(:all) do + class SampleTenant < ActiveRecord::Base + multi_tenant :sample_tenant + end + end + + it 'return true with valid tenant_name' do + expect(MultiTenant.tenant_klass_defined?(:sample_tenant)).to eq(true) + end + + it 'return false with invalid_tenant_name' do + invalid_tenant_name = :tenant + expect(MultiTenant.tenant_klass_defined?(invalid_tenant_name)).to eq(false) + end + end + + context 'with options' do + context 'and valid class_name' do + it 'return true' do + class SampleTenant < ActiveRecord::Base + multi_tenant :tenant + end + + tenant_name = :tenant + options = { + class_name: 'SampleTenant' + } + expect(MultiTenant.tenant_klass_defined?(tenant_name, options)).to eq(true) + end + + it 'return true when tenant class is nested' do + module SampleModule + class SampleNestedTenant < ActiveRecord::Base + multi_tenant :tenant + end + # rubocop:disable Layout/TrailingWhitespace + # Trailing whitespace is intentionally left here + + class AnotherTenant < ActiveRecord::Base + end + # rubocop:enable Layout/TrailingWhitespace + end + tenant_name = :tenant + options = { + class_name: 'SampleModule::SampleNestedTenant' + } + expect(MultiTenant.tenant_klass_defined?(tenant_name, options)).to eq(true) + end + end + end + end end From 5217915f9c9616c158c40a5ede9d80fb6db9dd27 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Jul 2023 12:43:22 +0000 Subject: [PATCH 091/105] Bump certifi from 2023.5.7 to 2023.7.22 in /docs (#204) Bumps [certifi](https://github.com/certifi/python-certifi) from 2023.5.7 to 2023.7.22. - [Commits](https://github.com/certifi/python-certifi/compare/2023.05.07...2023.07.22) --- updated-dependencies: - dependency-name: certifi dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 296e53c1..3e991e42 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -8,7 +8,7 @@ alabaster==0.7.13 # via sphinx babel==2.12.1 # via sphinx -certifi==2023.5.7 +certifi==2023.7.22 # via requests charset-normalizer==3.1.0 # via requests From 100ac67d141d51414f99ff8fec71e041cbb80318 Mon Sep 17 00:00:00 2001 From: Ladislav Gallay Date: Thu, 7 Sep 2023 13:58:36 +0200 Subject: [PATCH 092/105] Fix missing scope in habtm.rb (#207) Fixes #206 --- lib/activerecord-multi-tenant/habtm.rb | 4 ++-- spec/schema.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/activerecord-multi-tenant/habtm.rb b/lib/activerecord-multi-tenant/habtm.rb index 8c0edf94..5cba8144 100644 --- a/lib/activerecord-multi-tenant/habtm.rb +++ b/lib/activerecord-multi-tenant/habtm.rb @@ -8,9 +8,9 @@ module ActiveRecord module Associations module ClassMethods # rubocop:disable Naming/PredicateName - def has_and_belongs_to_many_with_tenant(name, options = {}, &extension) + def has_and_belongs_to_many_with_tenant(name, scope = nil, **options, &extension) # rubocop:enable Naming/PredicateName - has_and_belongs_to_many_without_tenant(name, **options, &extension) + has_and_belongs_to_many_without_tenant(name, scope, **options, &extension) middle_reflection = _reflections[name.to_s].through_reflection join_model = middle_reflection.klass diff --git a/spec/schema.rb b/spec/schema.rb index 93c6fd74..517cf965 100644 --- a/spec/schema.rb +++ b/spec/schema.rb @@ -174,8 +174,8 @@ class Project < ActiveRecord::Base class Manager < ActiveRecord::Base multi_tenant :account belongs_to :project - has_and_belongs_to_many :tasks, { tenant_column: :account_id, tenant_enabled: true, - tenant_class_name: 'Account' } + has_and_belongs_to_many :tasks, tenant_column: :account_id, tenant_enabled: true, + tenant_class_name: 'Account' end class Task < ActiveRecord::Base From 648f353663f4bbe1d927d71fd71b88b890316632 Mon Sep 17 00:00:00 2001 From: alpaca-tc Date: Thu, 14 Sep 2023 15:35:59 +0900 Subject: [PATCH 093/105] Support rails 7.1 (#208) * Add rails 7.1.0.beta1 * Support Rails7.1 The `table_name` method was defined in `Arel::Nodes::Table` and `Arel::Nodes::TableAlias` to get the table name, but `Arel::Nodes::Table#table_name` was removed rails/rails#46864. Therefore, it is no longer possible to simply call `#table_name` to get `table_name`, so a `TableNode.table_name` has been added to get table_name from node. --- .../active-record-multi-tenant-tests.yml | 4 +++- Appraisals | 8 ++++++++ lib/activerecord-multi-tenant/multi_tenant.rb | 4 ++-- .../query_rewriter.rb | 20 ++++++++++--------- lib/activerecord-multi-tenant/table_node.rb | 13 ++++++++++++ lib/activerecord_multi_tenant.rb | 1 + 6 files changed, 38 insertions(+), 12 deletions(-) create mode 100644 lib/activerecord-multi-tenant/table_node.rb diff --git a/.github/workflows/active-record-multi-tenant-tests.yml b/.github/workflows/active-record-multi-tenant-tests.yml index 1d7e09c2..079a966a 100644 --- a/.github/workflows/active-record-multi-tenant-tests.yml +++ b/.github/workflows/active-record-multi-tenant-tests.yml @@ -51,13 +51,15 @@ jobs: - rails-6.0 - rails-6.1 - rails-7.0 + - rails-7.1 - active-record-6.0 - active-record-6.1 - active-record-7.0 + - active-record-7.1 citus_version: - '10' - '11' - + name: Ruby ${{ matrix.ruby }}/${{ matrix.gemfile }} / Citus ${{ matrix.citus_version }} env: APPRAISAL: ${{ matrix.appraisal }} diff --git a/Appraisals b/Appraisals index abac38c4..1fc210d3 100644 --- a/Appraisals +++ b/Appraisals @@ -10,6 +10,10 @@ appraise 'rails-7.0' do gem 'rails', '~> 7.0.0' end +appraise 'rails-7.1' do + gem 'rails', '~> 7.1.0.beta1' +end + appraise 'active-record-6.0' do gem 'activerecord', '~> 6.0.3' end @@ -21,3 +25,7 @@ end appraise 'active-record-7.0' do gem 'activerecord', '~> 7.0.0' end + +appraise 'active-record-7.1' do + gem 'activerecord', '~> 7.1.0.beta1' +end diff --git a/lib/activerecord-multi-tenant/multi_tenant.rb b/lib/activerecord-multi-tenant/multi_tenant.rb index b541e33a..c4b24932 100644 --- a/lib/activerecord-multi-tenant/multi_tenant.rb +++ b/lib/activerecord-multi-tenant/multi_tenant.rb @@ -63,9 +63,9 @@ def self.multi_tenant_model_for_arel(arel) return nil unless arel.respond_to?(:ast) if arel.ast.relation.is_a? Arel::Nodes::JoinSource - MultiTenant.multi_tenant_model_for_table(arel.ast.relation.left.table_name) + MultiTenant.multi_tenant_model_for_table(TableNode.table_name(arel.ast.relation.left)) else - MultiTenant.multi_tenant_model_for_table(arel.ast.relation.table_name) + MultiTenant.multi_tenant_model_for_table(TableNode.table_name(arel.ast.relation)) end end diff --git a/lib/activerecord-multi-tenant/query_rewriter.rb b/lib/activerecord-multi-tenant/query_rewriter.rb index c8bbca9e..d48170a7 100644 --- a/lib/activerecord-multi-tenant/query_rewriter.rb +++ b/lib/activerecord-multi-tenant/query_rewriter.rb @@ -83,7 +83,7 @@ def visit_Arel_Attributes_Attribute(*args) def visit_Arel_Nodes_Equality(obj, *args) if obj.left.is_a?(Arel::Attributes::Attribute) - table_name = obj.left.relation.table_name + table_name = MultiTenant::TableNode.table_name(obj.left.relation) model = MultiTenant.multi_tenant_model_for_table(table_name) if model.present? && obj.left.name.to_s == model.partition_key.to_s @current_context.visited_handled_relation(obj.left.relation) @@ -101,7 +101,7 @@ def visit_MultiTenant_TenantJoinEnforcementClause(obj, *) end def visit_Arel_Table(obj, _collector = nil) - @current_context.visited_relation(obj) if tenant_relation?(obj.table_name) + @current_context.visited_relation(obj) if tenant_relation?(MultiTenant::TableNode.table_name(obj)) end alias visit_Arel_Nodes_TableAlias visit_Arel_Table @@ -179,7 +179,9 @@ class BaseTenantEnforcementClause < Arel::Nodes::Node def initialize(tenant_attribute) super() @tenant_attribute = tenant_attribute - @tenant_model = MultiTenant.multi_tenant_model_for_table(tenant_attribute.relation.table_name) + @tenant_model = MultiTenant.multi_tenant_model_for_table( + MultiTenant::TableNode.table_name(tenant_attribute.relation) + ) end def to_s @@ -215,7 +217,7 @@ class TenantJoinEnforcementClause < BaseTenantEnforcementClause def initialize(tenant_attribute, table_left) super(tenant_attribute) @table_left = table_left - @model_left = MultiTenant.multi_tenant_model_for_table(table_left.table_name) + @model_left = MultiTenant.multi_tenant_model_for_table(MultiTenant::TableNode.table_name(table_left)) end private @@ -241,7 +243,7 @@ def visit_MultiTenant_TenantJoinEnforcementClause(obj, collector) module DatabaseStatements def join_to_update(update, *args) update = super(update, *args) - model = MultiTenant.multi_tenant_model_for_table(update.ast.relation.table_name) + model = MultiTenant.multi_tenant_model_for_table(MultiTenant::TableNode.table_name(update.ast.relation)) if model.present? && !MultiTenant.with_write_only_mode_enabled? && MultiTenant.current_tenant_id.present? update.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key])) end @@ -250,7 +252,7 @@ def join_to_update(update, *args) def join_to_delete(delete, *args) delete = super(delete, *args) - model = MultiTenant.multi_tenant_model_for_table(delete.ast.left.table_name) + model = MultiTenant.multi_tenant_model_for_table(MultiTenant::TableNode.table_name(delete.ast.left)) if model.present? && !MultiTenant.with_write_only_mode_enabled? && MultiTenant.current_tenant_id.present? delete.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key])) end @@ -295,7 +297,7 @@ def build_arel(*args) node = context.arel_node context.unhandled_relations.each do |relation| - model = MultiTenant.multi_tenant_model_for_table(relation.arel_table.table_name) + model = MultiTenant.multi_tenant_model_for_table(MultiTenant::TableNode.table_name(relation.arel_table)) if MultiTenant.current_tenant_id enforcement_clause = MultiTenant::TenantEnforcementClause.new(relation.arel_table[model.partition_key]) @@ -330,8 +332,8 @@ def build_arel(*args) next unless relation_right && relation_left - model_right = MultiTenant.multi_tenant_model_for_table(relation_left.table_name) - model_left = MultiTenant.multi_tenant_model_for_table(relation_right.table_name) + model_right = MultiTenant.multi_tenant_model_for_table(MultiTenant::TableNode.table_name(relation_left)) + model_left = MultiTenant.multi_tenant_model_for_table(MultiTenant::TableNode.table_name(relation_right)) next unless model_right && model_left join_enforcement_clause = MultiTenant::TenantJoinEnforcementClause.new( diff --git a/lib/activerecord-multi-tenant/table_node.rb b/lib/activerecord-multi-tenant/table_node.rb new file mode 100644 index 00000000..61f34c2a --- /dev/null +++ b/lib/activerecord-multi-tenant/table_node.rb @@ -0,0 +1,13 @@ +module MultiTenant + module TableNode + # Return table name + def self.table_name(node) + # NOTE: Arel::Nodes::Table#table_name is removed in Rails 7.1 + if node.is_a?(Arel::Nodes::TableAlias) + node.table_name + else + node.name + end + end + end +end diff --git a/lib/activerecord_multi_tenant.rb b/lib/activerecord_multi_tenant.rb index 8e8e79d5..1b9b606d 100644 --- a/lib/activerecord_multi_tenant.rb +++ b/lib/activerecord_multi_tenant.rb @@ -8,5 +8,6 @@ require_relative 'activerecord-multi-tenant/multi_tenant' require_relative 'activerecord-multi-tenant/query_rewriter' require_relative 'activerecord-multi-tenant/query_monitor' +require_relative 'activerecord-multi-tenant/table_node' require_relative 'activerecord-multi-tenant/version' require_relative 'activerecord-multi-tenant/habtm' From cf1b4121520bc95f639f650829c4512128ee1d33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCrkan=20=C4=B0ndibay?= Date: Fri, 22 Sep 2023 16:49:04 +0300 Subject: [PATCH 094/105] Adds citus 12 to test matrix (#210) --- .github/workflows/active-record-multi-tenant-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/active-record-multi-tenant-tests.yml b/.github/workflows/active-record-multi-tenant-tests.yml index 079a966a..9e5ed6c0 100644 --- a/.github/workflows/active-record-multi-tenant-tests.yml +++ b/.github/workflows/active-record-multi-tenant-tests.yml @@ -59,6 +59,7 @@ jobs: citus_version: - '10' - '11' + - '12' name: Ruby ${{ matrix.ruby }}/${{ matrix.gemfile }} / Citus ${{ matrix.citus_version }} env: From 9939b5e8f76a6e59d29c3e35b083205864d7d479 Mon Sep 17 00:00:00 2001 From: gindibay Date: Fri, 22 Sep 2023 17:06:34 +0300 Subject: [PATCH 095/105] Bump version to 2.4.0 --- CHANGELOG.md | 7 +++++++ lib/activerecord-multi-tenant/version.rb | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c445f1c4..9a0ab514 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,11 @@ # Changelog + +## 2.4.0 2023-06-05 +* Adds citus 12 to test matrix (#210) +* Adds Support for rails 7.1 (#208) +* Fix missing scope in habtm.rb (#207) +* Update logic inside the tenant_klass_defined? method (#202) + ## 2.3.0 2023-06-05 * Adds has_and_belongs_to_many feature with tenant (#193) * Removes eol ruby versions diff --git a/lib/activerecord-multi-tenant/version.rb b/lib/activerecord-multi-tenant/version.rb index 20a076ea..07314436 100644 --- a/lib/activerecord-multi-tenant/version.rb +++ b/lib/activerecord-multi-tenant/version.rb @@ -1,3 +1,3 @@ module MultiTenant - VERSION = '2.3.0'.freeze + VERSION = '2.4.0'.freeze end From c19259477210a354592464c692da1f9a0fe05a72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCrkan=20=C4=B0ndibay?= Date: Mon, 2 Oct 2023 17:18:09 +0300 Subject: [PATCH 096/105] Update CHANGELOG.md (#211) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a0ab514..bb95949d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 2.4.0 2023-06-05 +## 2.4.0 2023-09-22 * Adds citus 12 to test matrix (#210) * Adds Support for rails 7.1 (#208) * Fix missing scope in habtm.rb (#207) From aa0e161cb50f013eabca3e400cffe16b54a587fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Oct 2023 12:35:16 +0000 Subject: [PATCH 097/105] Bump urllib3 from 2.0.2 to 2.0.6 in /docs (#212) Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.0.2 to 2.0.6. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/2.0.2...2.0.6) --- updated-dependencies: - dependency-name: urllib3 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 3e991e42..40a0ef5b 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -58,5 +58,5 @@ sphinxcontrib-serializinghtml==1.1.5 # via sphinx sphinxnotes-strike==1.2 # via -r requirements.in -urllib3==2.0.2 +urllib3==2.0.6 # via requests From 48e6a42fa8dfc78c692f75d4b9240fdfd07a69fe Mon Sep 17 00:00:00 2001 From: alpaca-tc Date: Mon, 9 Oct 2023 21:13:37 +0900 Subject: [PATCH 098/105] Remove beta1 from rails 7.1.0 (#213) --- Appraisals | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Appraisals b/Appraisals index 1fc210d3..58fc0dc2 100644 --- a/Appraisals +++ b/Appraisals @@ -11,7 +11,7 @@ appraise 'rails-7.0' do end appraise 'rails-7.1' do - gem 'rails', '~> 7.1.0.beta1' + gem 'rails', '~> 7.1.0' end appraise 'active-record-6.0' do @@ -27,5 +27,5 @@ appraise 'active-record-7.0' do end appraise 'active-record-7.1' do - gem 'activerecord', '~> 7.1.0.beta1' + gem 'activerecord', '~> 7.1.0' end From de48c9f35800923e5fc3c9630d276403929dca1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 15:21:46 +0000 Subject: [PATCH 099/105] Bump urllib3 from 2.0.6 to 2.0.7 in /docs (#214) Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.0.6 to 2.0.7. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/2.0.6...2.0.7) --- updated-dependencies: - dependency-name: urllib3 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 40a0ef5b..37de70e5 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -58,5 +58,5 @@ sphinxcontrib-serializinghtml==1.1.5 # via sphinx sphinxnotes-strike==1.2 # via -r requirements.in -urllib3==2.0.6 +urllib3==2.0.7 # via requests From f1f043dfd9687e2d15f8af33fb7ff095cd1d7d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Mon, 23 Oct 2023 13:41:41 +0000 Subject: [PATCH 100/105] Fix rubocop warnings (#215) Enable more --- .rubocop.yml | 18 ++++++++---- Appraisals | 2 ++ Gemfile | 2 ++ Rakefile | 2 ++ activerecord-multi-tenant.gemspec | 8 ++++-- .../copy_from_client.rb | 2 ++ .../fast_truncate.rb | 2 ++ lib/activerecord-multi-tenant/habtm.rb | 7 ++--- lib/activerecord-multi-tenant/migrations.rb | 2 ++ .../model_extensions.rb | 28 +++++++++++-------- lib/activerecord-multi-tenant/multi_tenant.rb | 2 ++ .../query_monitor.rb | 2 ++ .../query_rewriter.rb | 4 ++- lib/activerecord-multi-tenant/sidekiq.rb | 2 ++ lib/activerecord-multi-tenant/table_node.rb | 2 ++ lib/activerecord-multi-tenant/version.rb | 4 ++- .../associations_spec.rb | 2 ++ .../multi_tenant_spec.rb | 2 +- .../record_callback_spec.rb | 2 ++ .../record_finding_spec.rb | 2 ++ .../activerecord-multi-tenant/sidekiq_spec.rb | 2 ++ spec/spec_helper.rb | 4 +-- 22 files changed, 75 insertions(+), 28 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index de3b54a4..cb334d8b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -7,21 +7,29 @@ AllCops: - 'node_modules/**/*' - 'Vagrantfile' TargetRubyVersion: 3.0 + SuggestExtensions: false + NewCops: enable -Style/FrozenStringLiteralComment: +Gemspec/DevelopmentDependencies: Enabled: false -Style/Documentation: - Exclude: - - '**/*.rb' +Lint/ConstantDefinitionInBlock: Enabled: false -Lint/ConstantDefinitionInBlock: +Lint/EmptyBlock: Enabled: false Style/ClassAndModuleChildren: Enabled: false +Style/Documentation: + Exclude: + - '**/*.rb' + Enabled: false + +Style/DocumentDynamicEvalDefinition: + Enabled: false + Metrics/BlockLength: Max: 650 diff --git a/Appraisals b/Appraisals index 58fc0dc2..7b1f9086 100644 --- a/Appraisals +++ b/Appraisals @@ -1,3 +1,5 @@ +# frozen_string_literal: true + appraise 'rails-6.0' do gem 'rails', '~> 6.0.3' end diff --git a/Gemfile b/Gemfile index fe65166c..3ad7db9f 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source 'https://rubygems.org' gemspec diff --git a/Rakefile b/Rakefile index fb8d39a3..e5ff0a2a 100644 --- a/Rakefile +++ b/Rakefile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rubygems' require 'bundler/setup' require 'bundler/gem_tasks' diff --git a/activerecord-multi-tenant.gemspec b/activerecord-multi-tenant.gemspec index a90fc884..0811991c 100644 --- a/activerecord-multi-tenant.gemspec +++ b/activerecord-multi-tenant.gemspec @@ -1,18 +1,20 @@ +# frozen_string_literal: true + $LOAD_PATH.push File.expand_path('lib', __dir__) require 'activerecord-multi-tenant/version' Gem::Specification.new do |spec| spec.name = 'activerecord-multi-tenant' spec.version = MultiTenant::VERSION - spec.summary = 'ActiveRecord/Rails integration for multi-tenant databases, '\ - 'in particular the Citus extension for PostgreSQL' + spec.summary = 'ActiveRecord/Rails integration for multi-tenant databases, ' \ + 'in particular the Citus extension for PostgreSQL' spec.description = '' spec.authors = ['Citus Data'] spec.email = 'engage@citusdata.com' spec.required_ruby_version = '>= 3.0.0' + spec.metadata = { 'rubygems_mfa_required' => 'true' } spec.files = `git ls-files`.split("\n") - spec.test_files = `git ls-files -- {spec}/*`.split("\n") spec.require_paths = ['lib'] spec.homepage = 'https://github.com/citusdata/activerecord-multi-tenant' spec.license = 'MIT' diff --git a/lib/activerecord-multi-tenant/copy_from_client.rb b/lib/activerecord-multi-tenant/copy_from_client.rb index 786c7622..8df5b606 100644 --- a/lib/activerecord-multi-tenant/copy_from_client.rb +++ b/lib/activerecord-multi-tenant/copy_from_client.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module MultiTenant # Designed to be mixed into an ActiveRecord model to provide # a copy_from_client method that allows for efficient bulk insertion of diff --git a/lib/activerecord-multi-tenant/fast_truncate.rb b/lib/activerecord-multi-tenant/fast_truncate.rb index 27b61708..fdd5d97d 100644 --- a/lib/activerecord-multi-tenant/fast_truncate.rb +++ b/lib/activerecord-multi-tenant/fast_truncate.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Truncates only the tables that have been modified, according to sequence # values # Faster alternative to DatabaseCleaner.clean_with(:truncation, pre_count: true) diff --git a/lib/activerecord-multi-tenant/habtm.rb b/lib/activerecord-multi-tenant/habtm.rb index 5cba8144..31618451 100644 --- a/lib/activerecord-multi-tenant/habtm.rb +++ b/lib/activerecord-multi-tenant/habtm.rb @@ -34,11 +34,10 @@ def has_and_belongs_to_many_with_tenant(name, scope = nil, **options, &extension # This method sets the tenant_id on the join table and executes before creation of the join table record. define_method :tenant_set do - if tenant_enabled - raise MultiTenant::MissingTenantError, 'Tenant Id is not set' unless MultiTenant.current_tenant_id + return unless tenant_enabled + raise MultiTenant::MissingTenantError, 'Tenant Id is not set' unless MultiTenant.current_tenant_id - send("#{tenant_column}=", MultiTenant.current_tenant_id) - end + send("#{tenant_column}=", MultiTenant.current_tenant_id) end end end diff --git a/lib/activerecord-multi-tenant/migrations.rb b/lib/activerecord-multi-tenant/migrations.rb index 237e5202..daac3b29 100644 --- a/lib/activerecord-multi-tenant/migrations.rb +++ b/lib/activerecord-multi-tenant/migrations.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module MultiTenant module MigrationExtensions def create_distributed_table(table_name, partition_key) diff --git a/lib/activerecord-multi-tenant/model_extensions.rb b/lib/activerecord-multi-tenant/model_extensions.rb index 20566519..372d47d9 100644 --- a/lib/activerecord-multi-tenant/model_extensions.rb +++ b/lib/activerecord-multi-tenant/model_extensions.rb @@ -1,4 +1,6 @@ -require_relative './multi_tenant' +# frozen_string_literal: true + +require_relative 'multi_tenant' module MultiTenant # Extension to the model to allow scoping of models to the current tenant. This is done by adding @@ -6,7 +8,7 @@ module MultiTenant # model declaration. # Adds scoped_by_tenant? partition_key, primary_key and inherited methods to the model module ModelExtensionsClassMethods - DEFAULT_ID_FIELD = 'id'.freeze + DEFAULT_ID_FIELD = 'id' # executes when multi_tenant method is called in the model. This method adds the following # methods to the model that calls it. # scoped_by_tenant? - returns true if the model is scoped by tenant @@ -188,17 +190,21 @@ def inherited(subclass) end # skips statement caching for classes that is Multi-tenant or has a multi-tenant relation -class ActiveRecord::Associations::Association - alias skip_statement_cache_orig skip_statement_cache? +module ActiveRecord + module Associations + class Association + alias skip_statement_cache_orig skip_statement_cache? - def skip_statement_cache?(*scope) - return true if klass.respond_to?(:scoped_by_tenant?) && klass.scoped_by_tenant? + def skip_statement_cache?(*scope) + return true if klass.respond_to?(:scoped_by_tenant?) && klass.scoped_by_tenant? - if reflection.through_reflection - through_klass = reflection.through_reflection.klass - return true if through_klass.respond_to?(:scoped_by_tenant?) && through_klass.scoped_by_tenant? - end + if reflection.through_reflection + through_klass = reflection.through_reflection.klass + return true if through_klass.respond_to?(:scoped_by_tenant?) && through_klass.scoped_by_tenant? + end - skip_statement_cache_orig(*scope) + skip_statement_cache_orig(*scope) + end + end end end diff --git a/lib/activerecord-multi-tenant/multi_tenant.rb b/lib/activerecord-multi-tenant/multi_tenant.rb index c4b24932..1e929a45 100644 --- a/lib/activerecord-multi-tenant/multi_tenant.rb +++ b/lib/activerecord-multi-tenant/multi_tenant.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'active_support/current_attributes' module MultiTenant diff --git a/lib/activerecord-multi-tenant/query_monitor.rb b/lib/activerecord-multi-tenant/query_monitor.rb index 85b555f7..8af77b35 100644 --- a/lib/activerecord-multi-tenant/query_monitor.rb +++ b/lib/activerecord-multi-tenant/query_monitor.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Add generic warning when queries fail and there is no tenant set # To handle this case, a QueryMonitor hook is created and registered # to sql.active_record. This hook will log a warning when a query fails diff --git a/lib/activerecord-multi-tenant/query_rewriter.rb b/lib/activerecord-multi-tenant/query_rewriter.rb index d48170a7..8e66333e 100644 --- a/lib/activerecord-multi-tenant/query_rewriter.rb +++ b/lib/activerecord-multi-tenant/query_rewriter.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require 'active_record' -require_relative './arel_visitors_depth_first' unless Arel::Visitors.const_defined?(:DepthFirst) +require_relative 'arel_visitors_depth_first' unless Arel::Visitors.const_defined?(:DepthFirst) # Iterates AST and adds tenant enforcement clauses to all relations module MultiTenant diff --git a/lib/activerecord-multi-tenant/sidekiq.rb b/lib/activerecord-multi-tenant/sidekiq.rb index 6bbb5d29..0bca5257 100644 --- a/lib/activerecord-multi-tenant/sidekiq.rb +++ b/lib/activerecord-multi-tenant/sidekiq.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'sidekiq/client' # Adds methods to handle tenant information both in the client and server. diff --git a/lib/activerecord-multi-tenant/table_node.rb b/lib/activerecord-multi-tenant/table_node.rb index 61f34c2a..9431b06c 100644 --- a/lib/activerecord-multi-tenant/table_node.rb +++ b/lib/activerecord-multi-tenant/table_node.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module MultiTenant module TableNode # Return table name diff --git a/lib/activerecord-multi-tenant/version.rb b/lib/activerecord-multi-tenant/version.rb index 07314436..20299e68 100644 --- a/lib/activerecord-multi-tenant/version.rb +++ b/lib/activerecord-multi-tenant/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module MultiTenant - VERSION = '2.4.0'.freeze + VERSION = '2.4.0' end diff --git a/spec/activerecord-multi-tenant/associations_spec.rb b/spec/activerecord-multi-tenant/associations_spec.rb index 731e7b58..d74cd24c 100644 --- a/spec/activerecord-multi-tenant/associations_spec.rb +++ b/spec/activerecord-multi-tenant/associations_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe MultiTenant, 'Association methods' do diff --git a/spec/activerecord-multi-tenant/multi_tenant_spec.rb b/spec/activerecord-multi-tenant/multi_tenant_spec.rb index cedd0bbe..e18ba703 100644 --- a/spec/activerecord-multi-tenant/multi_tenant_spec.rb +++ b/spec/activerecord-multi-tenant/multi_tenant_spec.rb @@ -4,7 +4,7 @@ RSpec.describe MultiTenant do describe '.load_current_tenant!' do - let(:fake_tenant) { OpenStruct.new(id: 1) } + let(:fake_tenant) { double(id: 1) } let(:mock_klass) { double(find: fake_tenant) } before do diff --git a/spec/activerecord-multi-tenant/record_callback_spec.rb b/spec/activerecord-multi-tenant/record_callback_spec.rb index 4a7e64f0..816fb678 100644 --- a/spec/activerecord-multi-tenant/record_callback_spec.rb +++ b/spec/activerecord-multi-tenant/record_callback_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' class ProjectWithCallbacks < ActiveRecord::Base diff --git a/spec/activerecord-multi-tenant/record_finding_spec.rb b/spec/activerecord-multi-tenant/record_finding_spec.rb index 99873d1b..d5b46074 100644 --- a/spec/activerecord-multi-tenant/record_finding_spec.rb +++ b/spec/activerecord-multi-tenant/record_finding_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe MultiTenant, 'Record finding' do diff --git a/spec/activerecord-multi-tenant/sidekiq_spec.rb b/spec/activerecord-multi-tenant/sidekiq_spec.rb index b47dee90..30b9c792 100644 --- a/spec/activerecord-multi-tenant/sidekiq_spec.rb +++ b/spec/activerecord-multi-tenant/sidekiq_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'sidekiq/client' require 'activerecord-multi-tenant/sidekiq' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 2fdb9947..28628ff3 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -48,9 +48,9 @@ class Application < Rails::Application; end require 'bundler' Bundler.require(:default, :development) -require_relative './support/format_sql' +require_relative 'support/format_sql' -dbconfig = YAML.safe_load(IO.read(File.join(File.dirname(__FILE__), 'database.yml'))) +dbconfig = YAML.safe_load_file(File.join(File.dirname(__FILE__), 'database.yml')) ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), 'debug.log')) ActiveRecord::Base.establish_connection(dbconfig['test']) From d23bc78f1636b50a1326150234ea5106eaa051ab Mon Sep 17 00:00:00 2001 From: alpaca-tc Date: Fri, 3 Nov 2023 02:43:12 +0900 Subject: [PATCH 101/105] Use prepend instead of alias (#216) --- lib/activerecord-multi-tenant/migrations.rb | 21 +++++------ .../model_extensions.rb | 24 ++++++------- lib/activerecord-multi-tenant/multi_tenant.rb | 36 +++++-------------- .../query_rewriter.rb | 14 ++++---- .../multi_tenant_spec.rb | 28 +++++++++++++++ 5 files changed, 64 insertions(+), 59 deletions(-) diff --git a/lib/activerecord-multi-tenant/migrations.rb b/lib/activerecord-multi-tenant/migrations.rb index daac3b29..cd069705 100644 --- a/lib/activerecord-multi-tenant/migrations.rb +++ b/lib/activerecord-multi-tenant/migrations.rb @@ -68,22 +68,19 @@ def citus_version ActiveRecord::Migration.include MultiTenant::MigrationExtensions if defined?(ActiveRecord::Migration) -module ActiveRecord - module ConnectionAdapters # :nodoc: - module SchemaStatements - alias orig_create_table create_table - - def create_table(table_name, options = {}, &block) - ret = orig_create_table(table_name, **options.except(:partition_key), &block) - if options[:id] != false && options[:partition_key] && options[:partition_key].to_s != 'id' - execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{table_name}_pkey" - execute "ALTER TABLE #{table_name} ADD PRIMARY KEY(\"#{options[:partition_key]}\", id)" - end - ret +module MultiTenant + module SchemaStatementsExtensions + def create_table(table_name, options = {}, &block) + ret = super(table_name, **options.except(:partition_key), &block) + if options[:id] != false && options[:partition_key] && options[:partition_key].to_s != 'id' + execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{table_name}_pkey" + execute "ALTER TABLE #{table_name} ADD PRIMARY KEY(\"#{options[:partition_key]}\", id)" end + ret end end end +ActiveRecord::ConnectionAdapters::SchemaStatements.prepend(MultiTenant::SchemaStatementsExtensions) module ActiveRecord class SchemaDumper diff --git a/lib/activerecord-multi-tenant/model_extensions.rb b/lib/activerecord-multi-tenant/model_extensions.rb index 372d47d9..f1b2cfee 100644 --- a/lib/activerecord-multi-tenant/model_extensions.rb +++ b/lib/activerecord-multi-tenant/model_extensions.rb @@ -190,21 +190,19 @@ def inherited(subclass) end # skips statement caching for classes that is Multi-tenant or has a multi-tenant relation -module ActiveRecord - module Associations - class Association - alias skip_statement_cache_orig skip_statement_cache? - - def skip_statement_cache?(*scope) - return true if klass.respond_to?(:scoped_by_tenant?) && klass.scoped_by_tenant? - - if reflection.through_reflection - through_klass = reflection.through_reflection.klass - return true if through_klass.respond_to?(:scoped_by_tenant?) && through_klass.scoped_by_tenant? - end +module MultiTenant + module AssociationExtensions + def skip_statement_cache?(*scope) + return true if klass.respond_to?(:scoped_by_tenant?) && klass.scoped_by_tenant? - skip_statement_cache_orig(*scope) + if reflection.through_reflection + through_klass = reflection.through_reflection.klass + return true if through_klass.respond_to?(:scoped_by_tenant?) && through_klass.scoped_by_tenant? end + + super(*scope) end end end + +ActiveRecord::Associations::Association.prepend(MultiTenant::AssociationExtensions) diff --git a/lib/activerecord-multi-tenant/multi_tenant.rb b/lib/activerecord-multi-tenant/multi_tenant.rb index 1e929a45..91abba8d 100644 --- a/lib/activerecord-multi-tenant/multi_tenant.rb +++ b/lib/activerecord-multi-tenant/multi_tenant.rb @@ -130,39 +130,21 @@ def self.without(&block) # 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 # Instruments the methods provided with previously set Multitenant parameters - # In Ruby 2 using splat (*) operator with `&block` is not supported, so we need to use `method(...)` syntax # TODO: Could not understand the use of owner here. Need to check - if Gem::Version.create(RUBY_VERSION) < Gem::Version.new('3.0.0') - def self.wrap_methods(klass, owner, *method_names) - method_names.each do |method_name| - original_method_name = :"_mt_original_#{method_name}" - klass.class_eval <<-CODE, __FILE__, __LINE__ + 1 - alias_method :#{original_method_name}, :#{method_name} - def #{method_name}(*args, &block) - 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) - MultiTenant.with(#{owner}.public_send(#{owner}.class.partition_key)) { #{original_method_name}(*args, &block) } - else - #{original_method_name}(*args, &block) - end - end - CODE - end - end - else - def self.wrap_methods(klass, owner, *method_names) - method_names.each do |method_name| - original_method_name = :"_mt_original_#{method_name}" - klass.class_eval <<-CODE, __FILE__, __LINE__ + 1 - alias_method :#{original_method_name}, :#{method_name} + def self.wrap_methods(klass, owner, *method_names) + mod = Module.new + klass.prepend(mod) + + method_names.each do |method_name| + mod.module_eval <<-CODE, __FILE__, __LINE__ + 1 def #{method_name}(...) 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) - MultiTenant.with(#{owner}.public_send(#{owner}.class.partition_key)) { #{original_method_name}(...) } + MultiTenant.with(#{owner}.public_send(#{owner}.class.partition_key)) { super } else - #{original_method_name}(...) + super end end - CODE - end + CODE end end diff --git a/lib/activerecord-multi-tenant/query_rewriter.rb b/lib/activerecord-multi-tenant/query_rewriter.rb index 8e66333e..f516ffa1 100644 --- a/lib/activerecord-multi-tenant/query_rewriter.rb +++ b/lib/activerecord-multi-tenant/query_rewriter.rb @@ -284,13 +284,10 @@ def delete(arel, name = nil, binds = []) Arel::Visitors::ToSql.include(MultiTenant::TenantValueVisitor) -require 'active_record/relation' -module ActiveRecord - module QueryMethods - alias build_arel_orig build_arel - - def build_arel(*args) - arel = build_arel_orig(*args) +module MultiTenant + module QueryMethodsExtensions + def build_arel(*) + arel = super unless MultiTenant.with_write_only_mode_enabled? visitor = MultiTenant::ArelTenantVisitor.new(arel) @@ -374,6 +371,9 @@ def relations_from_node_join(node_join) end end +require 'active_record/relation' +ActiveRecord::QueryMethods.prepend(MultiTenant::QueryMethodsExtensions) + module MultiTenantFindBy def cached_find_by_statement(key, &block) return super unless respond_to?(:scoped_by_tenant?) && scoped_by_tenant? diff --git a/spec/activerecord-multi-tenant/multi_tenant_spec.rb b/spec/activerecord-multi-tenant/multi_tenant_spec.rb index e18ba703..f2f9fca9 100644 --- a/spec/activerecord-multi-tenant/multi_tenant_spec.rb +++ b/spec/activerecord-multi-tenant/multi_tenant_spec.rb @@ -118,4 +118,32 @@ class AnotherTenant < ActiveRecord::Base end end end + + describe '.wrap_methods' do + context 'when method is already prepended' do + it 'is not an stack error' do + klass = Class.new do + def hello + 'hello' + end + end + + klass.prepend(Module.new do + def hello + "#{super} world" + end + + def owner + Class.new(ActiveRecord::Base) do + self.table_name = 'accounts' + end.new + end + end) + + MultiTenant.wrap_methods(klass, :owner, :hello) + + expect(klass.new.hello).to eq('hello world') + end + end + end end From e33732a957e194d2e610bf4e0ab8f3a0a1a3e4a7 Mon Sep 17 00:00:00 2001 From: yuta25 Date: Sat, 2 Dec 2023 22:59:33 +0900 Subject: [PATCH 102/105] Fix issue to avoid errors when `table_name` is nil (#218) --- lib/activerecord-multi-tenant/model_extensions.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/activerecord-multi-tenant/model_extensions.rb b/lib/activerecord-multi-tenant/model_extensions.rb index f1b2cfee..40e9ca36 100644 --- a/lib/activerecord-multi-tenant/model_extensions.rb +++ b/lib/activerecord-multi-tenant/model_extensions.rb @@ -52,7 +52,8 @@ def primary_key @primary_key = if primary_object_keys.size == 1 primary_object_keys.first - elsif connection.schema_cache.columns_hash(table_name).include? DEFAULT_ID_FIELD + elsif table_name && + connection.schema_cache.columns_hash(table_name).include?(DEFAULT_ID_FIELD) DEFAULT_ID_FIELD end end From d91e62c6697bf2e381ce1d6b3d72b12b1fcbf0de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCrkan=20=C4=B0ndibay?= Date: Sun, 3 Dec 2023 04:38:57 +0300 Subject: [PATCH 103/105] Fixes static code analysis checks (#220) --- lib/activerecord-multi-tenant/model_extensions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/activerecord-multi-tenant/model_extensions.rb b/lib/activerecord-multi-tenant/model_extensions.rb index 40e9ca36..a2811130 100644 --- a/lib/activerecord-multi-tenant/model_extensions.rb +++ b/lib/activerecord-multi-tenant/model_extensions.rb @@ -79,7 +79,7 @@ def inherited(subclass) after_initialize proc { |record| if MultiTenant.current_tenant_id && (!record.attribute_present?(partition_key) || record.public_send(partition_key.to_sym).nil?) - record.public_send("#{partition_key}=".to_sym, MultiTenant.current_tenant_id) + record.public_send(:"#{partition_key}=", MultiTenant.current_tenant_id) end } From 20d09a83db160e40db8e526becf66f59ad65f9a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCrkan=20=C4=B0ndibay?= Date: Mon, 4 Dec 2023 08:43:36 +0300 Subject: [PATCH 104/105] Fixes Model.limit(n).delete_all & Model.limit(n).update_all generates incorrect query (#200) * Adds initial implementation but still failing * Users/amit909sin/issue 195 (#219) * Fix the tenant scoping for delete_all --------- Co-authored-by: amit909singh --- .../delete_operations.rb | 38 +++++++++++++++++++ lib/activerecord-multi-tenant/multi_tenant.rb | 2 +- lib/activerecord_multi_tenant.rb | 1 + .../query_rewriter_spec.rb | 27 +++++++++++++ 4 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 lib/activerecord-multi-tenant/delete_operations.rb diff --git a/lib/activerecord-multi-tenant/delete_operations.rb b/lib/activerecord-multi-tenant/delete_operations.rb new file mode 100644 index 00000000..b2bfbf3d --- /dev/null +++ b/lib/activerecord-multi-tenant/delete_operations.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Arel + module ActiveRecordRelationExtension + # Overrides the delete_all method to include tenant scoping + def delete_all + # Call the original delete_all method if the current tenant is identified by an ID + return super if MultiTenant.current_tenant_is_id? || MultiTenant.current_tenant.nil? + + tenant_key = MultiTenant.partition_key(MultiTenant.current_tenant_class) + tenant_id = MultiTenant.current_tenant_id + arel = eager_loading? ? apply_join_dependency.arel : build_arel + arel.source.left = table + + if tenant_id && klass.column_names.include?(tenant_key) + # Check if the tenant key is present in the model's column names + tenant_condition = table[tenant_key].eq(tenant_id) + # Add the tenant condition to the arel query if it is not already present + unless arel.constraints.any? { |node| node.to_sql.include?(tenant_condition.to_sql) } + arel = arel.where(tenant_condition) + end + end + + subquery = arel.clone + subquery.projections.clear + subquery = subquery.project(table[primary_key]) + in_condition = Arel::Nodes::In.new(table[primary_key], subquery.ast) + stmt = Arel::DeleteManager.new.from(table) + stmt.wheres = [in_condition] + + # Execute the delete statement using the connection and return the result + klass.connection.delete(stmt, "#{klass} Delete All").tap { reset } + end + end +end + +# Patch ActiveRecord::Relation with the extension module +ActiveRecord::Relation.prepend(Arel::ActiveRecordRelationExtension) diff --git a/lib/activerecord-multi-tenant/multi_tenant.rb b/lib/activerecord-multi-tenant/multi_tenant.rb index 91abba8d..e03008dc 100644 --- a/lib/activerecord-multi-tenant/multi_tenant.rb +++ b/lib/activerecord-multi-tenant/multi_tenant.rb @@ -17,7 +17,7 @@ def self.tenant_klass_defined?(tenant_name, options = {}) end def self.partition_key(tenant_name) - "#{tenant_name}_id" + "#{tenant_name.to_s.underscore}_id" end # rubocop:disable Style/ClassVars diff --git a/lib/activerecord_multi_tenant.rb b/lib/activerecord_multi_tenant.rb index 1b9b606d..09a29a0c 100644 --- a/lib/activerecord_multi_tenant.rb +++ b/lib/activerecord_multi_tenant.rb @@ -11,3 +11,4 @@ require_relative 'activerecord-multi-tenant/table_node' require_relative 'activerecord-multi-tenant/version' require_relative 'activerecord-multi-tenant/habtm' +require_relative 'activerecord-multi-tenant/delete_operations' diff --git a/spec/activerecord-multi-tenant/query_rewriter_spec.rb b/spec/activerecord-multi-tenant/query_rewriter_spec.rb index 5bc280cb..2b6410e3 100644 --- a/spec/activerecord-multi-tenant/query_rewriter_spec.rb +++ b/spec/activerecord-multi-tenant/query_rewriter_spec.rb @@ -45,12 +45,39 @@ let!(:manager1) { Manager.create(name: 'Manager 1', project: project1, account: account) } let!(:manager2) { Manager.create(name: 'Manager 2', project: project2, account: account) } + before(:each) do + @queries = [] + ActiveSupport::Notifications.subscribe('sql.active_record') do |_name, _started, _finished, _unique_id, payload| + @queries << payload[:sql] + end + end + + after(:each) do + ActiveSupport::Notifications.unsubscribe('sql.active_record') + end + it 'delete_all the records' do + expected_query = <<-SQL.strip + DELETE FROM "projects" WHERE "projects"."id" IN + (SELECT "projects"."id" FROM "projects" + INNER JOIN "managers" ON "managers"."project_id" = "projects"."id" + and "managers"."account_id" = :account_id + WHERE "projects"."account_id" = :account_id + ) + AND "projects"."account_id" = :account_id + SQL + expect do MultiTenant.with(account) do Project.joins(:manager).delete_all end end.to change { Project.count }.from(3).to(1) + + @queries.each do |actual_query| + next unless actual_query.include?('DELETE FROM ') + + expect(format_sql(actual_query)).to eq(format_sql(expected_query.gsub(':account_id', account.id.to_s))) + end end it 'delete_all the records without a current tenant' do From e3d2c88140e635fc9dbb75e87378d80484d419f3 Mon Sep 17 00:00:00 2001 From: amit909singh Date: Thu, 4 Jan 2024 23:23:41 -0800 Subject: [PATCH 105/105] Fix the tenant scoping for update_all (#223) --- ...te_operations.rb => relation_extension.rb} | 40 +++++-- lib/activerecord_multi_tenant.rb | 2 +- .../query_rewriter_spec.rb | 102 ++++++++++++++++++ 3 files changed, 136 insertions(+), 8 deletions(-) rename lib/activerecord-multi-tenant/{delete_operations.rb => relation_extension.rb} (50%) diff --git a/lib/activerecord-multi-tenant/delete_operations.rb b/lib/activerecord-multi-tenant/relation_extension.rb similarity index 50% rename from lib/activerecord-multi-tenant/delete_operations.rb rename to lib/activerecord-multi-tenant/relation_extension.rb index b2bfbf3d..c1d89002 100644 --- a/lib/activerecord-multi-tenant/delete_operations.rb +++ b/lib/activerecord-multi-tenant/relation_extension.rb @@ -7,29 +7,55 @@ def delete_all # Call the original delete_all method if the current tenant is identified by an ID return super if MultiTenant.current_tenant_is_id? || MultiTenant.current_tenant.nil? + stmt = Arel::DeleteManager.new.from(table) + stmt.wheres = [generate_in_condition_subquery] + + # Execute the delete statement using the connection and return the result + klass.connection.delete(stmt, "#{klass} Delete All").tap { reset } + end + + # Overrides the update_all method to include tenant scoping + def update_all(updates) + # Call the original update_all method if the current tenant is identified by an ID + return super if MultiTenant.current_tenant_is_id? || MultiTenant.current_tenant.nil? + + stmt = Arel::UpdateManager.new + stmt.table(table) + stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)) + stmt.wheres = [generate_in_condition_subquery] + + klass.connection.update(stmt, "#{klass} Update All").tap { reset } + end + + private + + # The generate_in_condition_subquery method generates a subquery that selects + # records associated with the current tenant. + def generate_in_condition_subquery + # Get the tenant key and tenant ID based on the current tenant tenant_key = MultiTenant.partition_key(MultiTenant.current_tenant_class) tenant_id = MultiTenant.current_tenant_id + + # Build an Arel query arel = eager_loading? ? apply_join_dependency.arel : build_arel arel.source.left = table + # If the tenant ID is present and the tenant key is a column in the model, + # add a condition to only include records where the tenant key equals the tenant ID if tenant_id && klass.column_names.include?(tenant_key) - # Check if the tenant key is present in the model's column names tenant_condition = table[tenant_key].eq(tenant_id) - # Add the tenant condition to the arel query if it is not already present unless arel.constraints.any? { |node| node.to_sql.include?(tenant_condition.to_sql) } arel = arel.where(tenant_condition) end end + # Clone the query, clear its projections, and set its projection to the primary key of the table subquery = arel.clone subquery.projections.clear subquery = subquery.project(table[primary_key]) - in_condition = Arel::Nodes::In.new(table[primary_key], subquery.ast) - stmt = Arel::DeleteManager.new.from(table) - stmt.wheres = [in_condition] - # Execute the delete statement using the connection and return the result - klass.connection.delete(stmt, "#{klass} Delete All").tap { reset } + # Create an IN condition node with the primary key of the table and the subquery + Arel::Nodes::In.new(table[primary_key], subquery.ast) end end end diff --git a/lib/activerecord_multi_tenant.rb b/lib/activerecord_multi_tenant.rb index 09a29a0c..7c4fc30b 100644 --- a/lib/activerecord_multi_tenant.rb +++ b/lib/activerecord_multi_tenant.rb @@ -11,4 +11,4 @@ require_relative 'activerecord-multi-tenant/table_node' require_relative 'activerecord-multi-tenant/version' require_relative 'activerecord-multi-tenant/habtm' -require_relative 'activerecord-multi-tenant/delete_operations' +require_relative 'activerecord-multi-tenant/relation_extension' diff --git a/spec/activerecord-multi-tenant/query_rewriter_spec.rb b/spec/activerecord-multi-tenant/query_rewriter_spec.rb index 2b6410e3..db0edda8 100644 --- a/spec/activerecord-multi-tenant/query_rewriter_spec.rb +++ b/spec/activerecord-multi-tenant/query_rewriter_spec.rb @@ -3,6 +3,13 @@ require 'spec_helper' describe 'Query Rewriter' do + before(:each) do + @queries = [] + ActiveSupport::Notifications.subscribe('sql.active_record') do |_name, _started, _finished, _unique_id, payload| + @queries << payload[:sql] + end + end + context 'when bulk updating' do let!(:account) { Account.create!(name: 'Test Account') } let!(:project) { Project.create(name: 'Project 1', account: account) } @@ -35,6 +42,67 @@ project.update(name: 'New Name') end.to change { project.reload.name }.from('Project 1').to('New Name') end + + it 'update_all the records with expected query' do + expected_query = <<-SQL.strip + UPDATE "projects" SET "name" = 'New Name' WHERE "projects"."id" IN + (SELECT "projects"."id" FROM "projects" + INNER JOIN "managers" ON "managers"."project_id" = "projects"."id" + and "managers"."account_id" = :account_id + WHERE "projects"."account_id" = :account_id + ) + AND "projects"."account_id" = :account_id + SQL + + expect do + MultiTenant.with(account) do + Project.joins(:manager).update_all(name: 'New Name') + end + end.to change { project.reload.name }.from('Project 1').to('New Name') + + @queries.each do |actual_query| + next unless actual_query.include?('UPDATE "projects" SET "name"') + + expect(format_sql(actual_query)).to eq(format_sql(expected_query.gsub(':account_id', account.id.to_s))) + end + end + + it 'updates a limited number of records with expected query' do + # create 2 more projects + Project.create(name: 'project2', account: account) + Project.create(name: 'project3', account: account) + new_name = 'New Name' + limit = 2 + expected_query = <<-SQL + UPDATE + "projects" + SET + "name" = 'New Name' + WHERE + "projects"."id" IN ( + SELECT + "projects"."id" + FROM + "projects" + WHERE + "projects"."account_id" = #{account.id} LIMIT #{limit} + ) + AND "projects"."account_id" = #{account.id} + SQL + + expect do + MultiTenant.with(account) do + Project.limit(limit).update_all(name: new_name) + end + end.to change { Project.where(name: new_name).count }.from(0).to(limit) + + @queries.each do |actual_query| + next unless actual_query.include?('UPDATE "projects" SET "name"') + + expect(format_sql(actual_query.gsub('$1', + limit.to_s)).strip).to eq(format_sql(expected_query).strip) + end + end end context 'when bulk deleting' do @@ -102,6 +170,40 @@ end.to change { Project.count }.from(3).to(1) end + it 'deletes a limited number of records with expected query' do + # create 2 more projects + Project.create(name: 'project2', account: account) + Project.create(name: 'project3', account: account) + limit = 2 + expected_query = <<-SQL + DELETE FROM + "projects" + WHERE + "projects"."id" IN ( + SELECT + "projects"."id" + FROM + "projects" + WHERE + "projects"."account_id" = #{account.id} LIMIT #{limit} + ) + AND "projects"."account_id" = #{account.id} + SQL + + expect do + MultiTenant.with(account) do + Project.limit(limit).delete_all + end + end.to change { Project.count }.by(-limit) + + @queries.each do |actual_query| + next unless actual_query.include?('DELETE FROM "projects"') + + expect(format_sql(actual_query.gsub('$1', + limit.to_s)).strip).to eq(format_sql(expected_query).strip) + end + end + it 'destroy the record' do expect do MultiTenant.with(account) do