activerecord-multi-tenant 1.0.4 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/CI.yml +63 -0
  3. data/.gitignore +2 -0
  4. data/.rspec +1 -0
  5. data/Appraisals +20 -47
  6. data/CHANGELOG.md +19 -0
  7. data/gemfiles/active_record_5.2.gemfile +3 -2
  8. data/gemfiles/active_record_6.0.gemfile +1 -1
  9. data/gemfiles/active_record_6.1.gemfile +8 -0
  10. data/gemfiles/active_record_7.0.gemfile +8 -0
  11. data/gemfiles/rails_5.2.gemfile +3 -2
  12. data/gemfiles/rails_6.0.gemfile +1 -1
  13. data/gemfiles/rails_6.1.gemfile +8 -0
  14. data/gemfiles/rails_7.0.gemfile +8 -0
  15. data/lib/activerecord-multi-tenant/arel_visitors_depth_first.rb +200 -0
  16. data/lib/activerecord-multi-tenant/controller_extensions.rb +2 -6
  17. data/lib/activerecord-multi-tenant/copy_from_client.rb +4 -4
  18. data/lib/activerecord-multi-tenant/migrations.rb +2 -2
  19. data/lib/activerecord-multi-tenant/model_extensions.rb +9 -9
  20. data/lib/activerecord-multi-tenant/multi_tenant.rb +1 -0
  21. data/lib/activerecord-multi-tenant/query_rewriter.rb +32 -75
  22. data/lib/activerecord-multi-tenant/sidekiq.rb +6 -1
  23. data/lib/activerecord-multi-tenant/version.rb +1 -1
  24. data/spec/activerecord-multi-tenant/controller_extensions_spec.rb +19 -24
  25. data/spec/activerecord-multi-tenant/model_extensions_spec.rb +58 -74
  26. data/spec/activerecord-multi-tenant/query_rewriter_spec.rb +25 -0
  27. data/spec/activerecord-multi-tenant/record_finding_spec.rb +36 -0
  28. data/spec/activerecord-multi-tenant/sidekiq_spec.rb +15 -4
  29. data/spec/schema.rb +28 -8
  30. data/spec/spec_helper.rb +1 -6
  31. metadata +13 -20
  32. data/.travis.yml +0 -57
  33. data/Gemfile.lock +0 -181
  34. data/gemfiles/active_record_5.1.gemfile +0 -15
  35. data/gemfiles/active_record_5.1.gemfile.lock +0 -180
  36. data/gemfiles/active_record_5.2.gemfile.lock +0 -188
  37. data/gemfiles/active_record_6.0.gemfile.lock +0 -198
  38. data/gemfiles/rails_4.2.gemfile +0 -16
  39. data/gemfiles/rails_4.2.gemfile.lock +0 -175
  40. data/gemfiles/rails_5.0.gemfile +0 -15
  41. data/gemfiles/rails_5.0.gemfile.lock +0 -180
  42. data/gemfiles/rails_5.1.gemfile +0 -15
  43. data/gemfiles/rails_5.1.gemfile.lock +0 -180
  44. data/gemfiles/rails_5.2.gemfile.lock +0 -188
  45. data/gemfiles/rails_6.0.gemfile.lock +0 -198
@@ -1,4 +1,5 @@
1
1
  require 'active_record'
2
+ require_relative "./arel_visitors_depth_first.rb" unless Arel::Visitors.const_defined?(:DepthFirst)
2
3
 
3
4
  module MultiTenant
4
5
  class Table
@@ -31,6 +32,7 @@ module MultiTenant
31
32
  @arel_node = arel_node
32
33
  @known_relations = []
33
34
  @handled_relations = []
35
+ @discovering = false
34
36
  end
35
37
 
36
38
  def discover_relations
@@ -54,7 +56,7 @@ module MultiTenant
54
56
  end
55
57
  end
56
58
 
57
- class ArelTenantVisitor < Arel::Visitors::DepthFirst
59
+ class ArelTenantVisitor < Arel::Visitors.const_defined?(:DepthFirst) ? Arel::Visitors::DepthFirst : ::MultiTenant::ArelVisitorsDepthFirst
58
60
  def initialize(arel)
59
61
  super(Proc.new {})
60
62
  @statement_node_id = nil
@@ -120,6 +122,8 @@ module MultiTenant
120
122
  alias :visit_Arel_Nodes_FullOuterJoin :visit_Arel_Nodes_OuterJoin
121
123
  alias :visit_Arel_Nodes_RightOuterJoin :visit_Arel_Nodes_OuterJoin
122
124
 
125
+ alias :visit_ActiveModel_Attribute :terminal
126
+
123
127
  private
124
128
 
125
129
  def tenant_relation?(table_name)
@@ -224,22 +228,20 @@ module MultiTenant
224
228
  delete
225
229
  end
226
230
 
227
- if (ActiveRecord::VERSION::MAJOR == 5 && ActiveRecord::VERSION::MINOR >= 2) || ActiveRecord::VERSION::MAJOR > 5
228
- def update(arel, name = nil, binds = [])
229
- model = MultiTenant.multi_tenant_model_for_arel(arel)
230
- if model.present? && !MultiTenant.with_write_only_mode_enabled? && MultiTenant.current_tenant_id.present?
231
- arel.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key]))
232
- end
233
- super(arel, name, binds)
231
+ def update(arel, name = nil, binds = [])
232
+ model = MultiTenant.multi_tenant_model_for_arel(arel)
233
+ if model.present? && !MultiTenant.with_write_only_mode_enabled? && MultiTenant.current_tenant_id.present?
234
+ arel.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key]))
234
235
  end
236
+ super(arel, name, binds)
237
+ end
235
238
 
236
- def delete(arel, name = nil, binds = [])
237
- model = MultiTenant.multi_tenant_model_for_arel(arel)
238
- if model.present? && !MultiTenant.with_write_only_mode_enabled? && MultiTenant.current_tenant_id.present?
239
- arel.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key]))
240
- end
241
- super(arel, name, binds)
239
+ def delete(arel, name = nil, binds = [])
240
+ model = MultiTenant.multi_tenant_model_for_arel(arel)
241
+ if model.present? && !MultiTenant.with_write_only_mode_enabled? && MultiTenant.current_tenant_id.present?
242
+ arel.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key]))
242
243
  end
244
+ super(arel, name, binds)
243
245
  end
244
246
  end
245
247
  end
@@ -274,7 +276,11 @@ module ActiveRecord
274
276
  if node.wheres.empty?
275
277
  node.wheres = [enforcement_clause]
276
278
  else
277
- node.wheres[0] = enforcement_clause.and(node.wheres[0])
279
+ if node.wheres[0].is_a?(Arel::Nodes::And)
280
+ node.wheres[0].children << enforcement_clause
281
+ else
282
+ node.wheres[0] = enforcement_clause.and(node.wheres[0])
283
+ end
278
284
  end
279
285
  else
280
286
  raise "UnknownContext"
@@ -290,7 +296,7 @@ module ActiveRecord
290
296
 
291
297
  node_list.select{ |n| n.is_a? Arel::Nodes::Join }.each do |node_join|
292
298
  if (!node_join.right ||
293
- (ActiveRecord::VERSION::MAJOR <= 5 &&
299
+ (ActiveRecord::VERSION::MAJOR == 5 &&
294
300
  !node_join.right.expr.right.is_a?(Arel::Attributes::Attribute)))
295
301
  next
296
302
  end
@@ -316,7 +322,7 @@ module ActiveRecord
316
322
 
317
323
  private
318
324
  def relations_from_node_join(node_join)
319
- if ActiveRecord::VERSION::MAJOR <= 5 || node_join.right.expr.is_a?(Arel::Nodes::Equality)
325
+ if ActiveRecord::VERSION::MAJOR == 5 || node_join.right.expr.is_a?(Arel::Nodes::Equality)
320
326
  return node_join.right.expr.right.relation, node_join.right.expr.left.relation
321
327
  end
322
328
 
@@ -327,71 +333,22 @@ module ActiveRecord
327
333
  return nil, nil
328
334
  end
329
335
 
330
- return children[0].right.relation, children[0].left.relation
336
+ if children[0].right.respond_to?('relation') && children[0].left.respond_to?('relation')
337
+ return children[0].right.relation, children[0].left.relation
338
+ end
339
+
340
+ return nil, nil
331
341
  end
332
342
  end
333
343
  end
334
344
 
335
345
  require 'active_record/base'
336
346
  module MultiTenantFindBy
337
- if ActiveRecord::VERSION::MAJOR == 4 && ActiveRecord::VERSION::MINOR >= 2
338
- # Disable caching for find and find_by in Rails 4.2 - we don't have a good
339
- # way to prevent caching problems here when prepared statements are enabled
340
- def find_by(*args)
341
- return super unless respond_to?(:scoped_by_tenant?) && scoped_by_tenant?
342
-
343
- # This duplicates a bunch of code from AR's find() method
344
- return super if current_scope || !(Hash === args.first) || reflect_on_all_aggregations.any?
345
- return super if default_scopes.any?
346
-
347
- hash = args.first
348
-
349
- return super if hash.values.any? { |v| v.nil? || Array === v || Hash === v }
350
- return super unless hash.keys.all? { |k| columns_hash.has_key?(k.to_s) }
351
-
352
- key = hash.keys
347
+ def cached_find_by_statement(key, &block)
348
+ return super unless respond_to?(:scoped_by_tenant?) && scoped_by_tenant?
353
349
 
354
- # Ensure we never use the cached version
355
- find_by_statement_cache.synchronize { find_by_statement_cache[key] = nil }
356
-
357
- super
358
- end
359
-
360
- def find(*ids)
361
- return super unless respond_to?(:scoped_by_tenant?) && scoped_by_tenant?
362
-
363
- # This duplicates a bunch of code from AR's find() method
364
- return super unless ids.length == 1
365
- return super if ids.first.kind_of?(Symbol)
366
- return super if block_given? ||
367
- primary_key.nil? ||
368
- default_scopes.any? ||
369
- current_scope ||
370
- columns_hash.include?(inheritance_column) ||
371
- ids.first.kind_of?(Array)
372
-
373
- id = ids.first
374
- if ActiveRecord::Base === id
375
- id = id.id
376
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
377
- You are passing an instance of ActiveRecord::Base to `find`.
378
- Please pass the id of the object by calling `.id`
379
- MSG
380
- end
381
- key = primary_key
382
-
383
- # Ensure we never use the cached version
384
- find_by_statement_cache.synchronize { find_by_statement_cache[key] = nil }
385
-
386
- super
387
- end
388
- elsif ActiveRecord::VERSION::MAJOR > 4
389
- def cached_find_by_statement(key, &block)
390
- return super unless respond_to?(:scoped_by_tenant?) && scoped_by_tenant?
391
-
392
- key = Array.wrap(key) + [MultiTenant.current_tenant_id.to_s]
393
- super(key, &block)
394
- end
350
+ key = Array.wrap(key) + [MultiTenant.current_tenant_id.to_s]
351
+ super(key, &block)
395
352
  end
396
353
  end
397
354
 
@@ -18,7 +18,12 @@ module Sidekiq::Middleware::MultiTenant
18
18
  class Server
19
19
  def call(worker_class, msg, queue)
20
20
  if msg.has_key?('multi_tenant')
21
- MultiTenant.with(msg['multi_tenant']['id']) do
21
+ tenant = begin
22
+ msg['multi_tenant']['class'].constantize.find(msg['multi_tenant']['id'])
23
+ rescue ActiveRecord::RecordNotFound
24
+ msg['multi_tenant']['id']
25
+ end
26
+ MultiTenant.with(tenant) do
22
27
  yield
23
28
  end
24
29
  else
@@ -1,3 +1,3 @@
1
1
  module MultiTenant
2
- VERSION = '1.0.4'
2
+ VERSION = '1.2.0'
3
3
  end
@@ -21,11 +21,7 @@ describe "Controller Extensions", type: :controller do
21
21
  describe ApplicationController, type: :controller do
22
22
  controller do
23
23
  def index
24
- if ActionPack::VERSION::MAJOR >= 5
25
- render body: 'custom called'
26
- else
27
- render text: 'custom called'
28
- end
24
+ render body: 'custom called'
29
25
  end
30
26
  end
31
27
 
@@ -35,30 +31,29 @@ describe "Controller Extensions", type: :controller do
35
31
  end
36
32
  end
37
33
 
38
- if ActionPack::VERSION::MAJOR >= 5
39
- class APIApplicationController < ActionController::API
40
- include Rails.application.routes.url_helpers
41
- set_current_tenant_through_filter
42
- before_action :your_method_that_finds_the_current_tenant
43
34
 
44
- def your_method_that_finds_the_current_tenant
45
- current_account = Account.new
46
- current_account.name = 'account1'
47
- set_current_tenant(current_account)
48
- end
35
+ class APIApplicationController < ActionController::API
36
+ include Rails.application.routes.url_helpers
37
+ set_current_tenant_through_filter
38
+ before_action :your_method_that_finds_the_current_tenant
39
+
40
+ def your_method_that_finds_the_current_tenant
41
+ current_account = Account.new
42
+ current_account.name = 'account1'
43
+ set_current_tenant(current_account)
49
44
  end
45
+ end
50
46
 
51
- describe APIApplicationController, type: :controller do
52
- controller do
53
- def index
54
- render body: 'custom called'
55
- end
47
+ describe APIApplicationController, type: :controller do
48
+ controller do
49
+ def index
50
+ render body: 'custom called'
56
51
  end
52
+ end
57
53
 
58
- it 'Finds the correct tenant using the filter command' do
59
- get :index
60
- expect(MultiTenant.current_tenant.name).to eq 'account1'
61
- end
54
+ it 'Finds the correct tenant using the filter command' do
55
+ get :index
56
+ expect(MultiTenant.current_tenant.name).to eq 'account1'
62
57
  end
63
58
  end
64
59
  end
@@ -348,9 +348,12 @@ describe MultiTenant do
348
348
  end
349
349
 
350
350
  it "applies the team_id conditions in the where clause" do
351
- expected_sql = <<-sql
352
- 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
353
- sql
351
+ option1 = <<-sql.strip
352
+ 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
353
+ sql
354
+ option2 = <<-sql.strip
355
+ 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
356
+ sql
354
357
 
355
358
  account1 = Account.create! name: 'Account 1'
356
359
 
@@ -358,7 +361,7 @@ describe MultiTenant do
358
361
  project1 = Project.create! name: 'Project 1'
359
362
  task1 = Task.create! name: 'Task 1', project: project1
360
363
  subtask1 = SubTask.create! task: task1
361
- expect(project1.sub_tasks.to_sql).to eq(expected_sql.strip)
364
+ expect(project1.sub_tasks.to_sql).to eq(option1).or(eq(option2))
362
365
  expect(project1.sub_tasks).to include(subtask1)
363
366
  end
364
367
 
@@ -373,9 +376,13 @@ describe MultiTenant do
373
376
  end
374
377
 
375
378
  it "tests joins between distributed and reference table" do
376
- expected_sql = <<-sql
377
- 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
378
- sql
379
+ option1 = <<-sql.strip
380
+ 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
381
+ sql
382
+ option2 = <<-sql.strip
383
+ 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
384
+ sql
385
+
379
386
  account1 = Account.create! name: 'Account 1'
380
387
  category1 = Category.create! name: 'Category 1'
381
388
 
@@ -383,7 +390,7 @@ describe MultiTenant do
383
390
  project1 = Project.create! name: 'Project 1'
384
391
  projectcategory = ProjectCategory.create! name: 'project cat 1', project: project1, category: category1
385
392
 
386
- expect(project1.categories.to_sql).to eq(expected_sql.strip)
393
+ expect(project1.categories.to_sql).to eq(option1).or(eq(option2))
387
394
  expect(project1.categories).to include(category1)
388
395
  expect(project1.project_categories).to include(projectcategory)
389
396
  end
@@ -412,25 +419,18 @@ describe MultiTenant do
412
419
  account1 = Account.create! name: 'Account 1'
413
420
  category1 = Category.create! name: 'Category 1'
414
421
 
415
- expected_sql = if uses_prepared_statements? && ActiveRecord::VERSION::MAJOR == 5 && ActiveRecord::VERSION::MINOR > 0
416
- <<-sql
417
- 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
418
- sql
419
- elsif uses_prepared_statements? && ActiveRecord::VERSION::MAJOR == 6
420
- <<-sql
421
- 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
422
- sql
423
- else
424
- <<-sql
425
- 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" 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
426
- sql
427
- end
422
+ option1 = <<-sql.strip
423
+ 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
424
+ sql
425
+ option2 = <<-sql.strip
426
+ 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
427
+ sql
428
428
 
429
429
  MultiTenant.with(account1) do
430
430
  project1 = Project.create! name: 'Project 1'
431
431
  projectcategory = ProjectCategory.create! name: 'project cat 1', project: project1, category: category1
432
432
 
433
- expect(Project.eager_load(:categories).to_sql).to eq(expected_sql.strip)
433
+ expect(Project.eager_load(:categories).to_sql).to eq(option1).or(eq(option2))
434
434
 
435
435
  project = Project.eager_load(:categories).first
436
436
  expect(project.categories).to include(category1)
@@ -455,25 +455,18 @@ describe MultiTenant do
455
455
  category1 = Category.create! name: 'Category 1'
456
456
 
457
457
  MultiTenant.with(account1) do
458
- expected_sql = if uses_prepared_statements? && ActiveRecord::VERSION::MAJOR == 5 && ActiveRecord::VERSION::MINOR > 0
459
- <<-sql
460
- 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
461
- sql
462
- elsif uses_prepared_statements? && ActiveRecord::VERSION::MAJOR == 6
463
- <<-sql
464
- 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
465
- sql
466
- else
467
- <<-sql
468
- 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 "projects"."account_id" = 1 AND "tasks"."account_id" = 1
469
- sql
470
- end
458
+ option1 = <<-sql.strip
459
+ 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
460
+ sql
461
+ option2 = <<-sql.strip
462
+ 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
463
+ sql
471
464
 
472
465
  project1 = Project.create! name: 'Project 1'
473
466
  projectcategory = ProjectCategory.create! name: 'project cat 1', project: project1, category: category1
474
467
 
475
468
  project1.tasks.create! name: 'baz'
476
- expect(Task.joins(:project).joins('LEFT JOIN project_categories pc ON project.category_id = pc.id').to_sql).to eq(expected_sql.strip)
469
+ expect(Task.joins(:project).joins('LEFT JOIN project_categories pc ON project.category_id = pc.id').to_sql).to eq(option1).or(eq(option2))
477
470
  end
478
471
 
479
472
  MultiTenant.without do
@@ -493,48 +486,39 @@ describe MultiTenant do
493
486
  project2 = Project.create! name: 'Project 2', account: Account.create!(name: 'Account2')
494
487
 
495
488
  MultiTenant.with(account) do
496
- expected_sql = if uses_prepared_statements? && ActiveRecord::VERSION::MAJOR > 5
497
- <<-sql.strip
498
- SELECT "projects".* FROM "projects" WHERE "projects"."account_id" = #{account.id} AND "projects"."id" = $1 LIMIT $2
499
- sql
500
- elsif uses_prepared_statements? && ActiveRecord::VERSION::MAJOR > 4
501
- <<-sql.strip
502
- SELECT "projects".* FROM "projects" WHERE "projects"."account_id" = #{account.id} AND "projects"."id" = $1 LIMIT $2
503
- sql
504
- elsif uses_prepared_statements? && ActiveRecord::VERSION::MAJOR == 4
505
- <<-sql.strip
506
- SELECT "projects".* FROM "projects" WHERE "projects"."account_id" = #{account.id} AND "projects"."id" = $1 LIMIT 1
507
- sql
508
- else
509
- <<-sql.strip
510
- SELECT "projects".* FROM "projects" WHERE "projects"."account_id" = #{account.id} AND "projects"."id" = #{project.id} LIMIT 1
511
- sql
512
- end
513
-
514
- expect(Project).to receive(:find_by_sql).with(expected_sql, any_args).and_call_original
489
+ option1 = <<-sql.strip
490
+ SELECT "projects".* FROM "projects" WHERE "projects"."account_id" = #{account.id} AND "projects"."id" = $1 LIMIT $2
491
+ sql
492
+ option2 = <<-sql.strip
493
+ SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 AND "projects"."account_id" = #{account.id} LIMIT $2
494
+ sql
495
+ option3 = <<-sql.strip
496
+ SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 AND "projects"."account_id" = #{account.id} LIMIT $2
497
+ sql
498
+
499
+ # Couldn't make the following line pass for some reason, so came up with an uglier alternative
500
+ # expect(Project).to receive(:find_by_sql).with(eq(option1).or(eq(option2)).or(eq(option3)), any_args).and_call_original
501
+ expect(Project).to receive(:find_by_sql).and_wrap_original do |m, *args|
502
+ expect(args[0]).to(eq(option1).or(eq(option2)).or(eq(option3)))
503
+ m.call(args[0], args[1], preparable:args[2][:preparable])
504
+ end
515
505
  expect(Project.find(project.id)).to eq(project)
516
506
  end
517
507
 
518
508
  MultiTenant.without do
519
- expected_sql = if uses_prepared_statements? && ActiveRecord::VERSION::MAJOR > 5
520
- <<-sql.strip
521
- SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2
522
- sql
523
- elsif uses_prepared_statements? && ActiveRecord::VERSION::MAJOR > 4
524
- <<-sql.strip
525
- SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2
526
- sql
527
- elsif uses_prepared_statements? && ActiveRecord::VERSION::MAJOR == 4
528
- <<-sql.strip
529
- SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT 1
530
- sql
531
- else
532
- <<-sql.strip
533
- SELECT "projects".* FROM "projects" WHERE "projects"."id" = #{project2.id} LIMIT 1
534
- sql
535
- end
536
-
537
- expect(Project).to receive(:find_by_sql).with(expected_sql, any_args).and_call_original
509
+ option1 = <<-sql.strip
510
+ SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2
511
+ sql
512
+ option2 = <<-sql.strip
513
+ SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2
514
+ sql
515
+
516
+ # Couldn't make the following line pass for some reason, so came up with an uglier alternative
517
+ # expect(Project).to receive(:find_by_sql).with(eq(option1).or(eq(option2)), any_args).and_call_original
518
+ expect(Project).to receive(:find_by_sql).and_wrap_original do |m, *args|
519
+ expect(args[0]).to(eq(option1).or(eq(option2)))
520
+ m.call(args[0], args[1], preparable:args[2][:preparable])
521
+ end
538
522
  expect(Project.find(project2.id)).to eq(project2)
539
523
  end
540
524
  end
@@ -90,4 +90,29 @@ describe "Query Rewriter" do
90
90
  }.to change { Project.count }.from(3).to(1)
91
91
  end
92
92
  end
93
+
94
+ context "when update without arel" do
95
+ it "can call method" do
96
+ expect {
97
+ ActiveRecord::Base.connection.update("SELECT 1")
98
+ }.not_to raise_error
99
+ end
100
+ end
101
+
102
+ context "when joining with a model with a default scope" do
103
+ let!(:account) { Account.create!(name: "Test Account") }
104
+
105
+ it "fetches only records within the default scope" do
106
+ alive = Domain.create(name: "alive", account: account)
107
+ deleted = Domain.create(name: "deleted", deleted: true, account: account)
108
+ page_in_alive_domain = Page.create(name: "alive", account: account, domain: alive)
109
+ page_in_deleted_domain = Page.create(name: "deleted", account: account, domain: deleted)
110
+
111
+ expect(
112
+ MultiTenant.with(account) do
113
+ Page.joins(:domain).pluck(:id)
114
+ end
115
+ ).to eq([page_in_alive_domain.id])
116
+ end
117
+ end
93
118
  end
@@ -59,4 +59,40 @@ describe MultiTenant, 'Record finding' do
59
59
  expect(second_found).to eq(second_record)
60
60
  end
61
61
  end
62
+
63
+ context 'model with has_many relation through multi-tenant model' do
64
+ let(:tenant_1) { Account.create! name: 'Tenant 1' }
65
+ let(:project_1) { tenant_1.projects.create! }
66
+
67
+ let(:tenant_2) { Account.create! name: 'Tenant 2' }
68
+ let(:project_2) { tenant_2.projects.create! }
69
+
70
+ let(:category) { Category.create! name: 'Category' }
71
+
72
+ before do
73
+ ProjectCategory.create! account: tenant_1, name: '1', project: project_1, category: category
74
+ ProjectCategory.create! account: tenant_2, name: '2', project: project_2, category: category
75
+ end
76
+
77
+ it 'can get model without creating query cache' do
78
+ MultiTenant.with(tenant_1) do
79
+ found_category = Project.find(project_1.id).categories.to_a.first
80
+ expect(found_category).to eq(category)
81
+ end
82
+ end
83
+
84
+ it 'can get model for other tenant' do
85
+ MultiTenant.with(tenant_2) do
86
+ found_category = Project.find(project_2.id).categories.to_a.first
87
+ expect(found_category).to eq(category)
88
+ end
89
+ end
90
+
91
+ it 'can get model without current_tenant' do
92
+ MultiTenant.without do
93
+ found_category = Project.find(project_2.id).categories.to_a.first
94
+ expect(found_category).to eq(category)
95
+ end
96
+ end
97
+ end
62
98
  end
@@ -4,14 +4,25 @@ require 'activerecord-multi-tenant/sidekiq'
4
4
 
5
5
  describe MultiTenant, 'Sidekiq' do
6
6
  let(:server) { Sidekiq::Middleware::MultiTenant::Server.new }
7
+ let(:account) { Account.create(name: 'test') }
8
+ let(:deleted_acount) { Account.create(name: 'deleted') }
9
+
10
+ before { deleted_acount.destroy! }
7
11
 
8
12
  describe 'server middleware' do
9
13
  it 'sets the multitenant context when provided in message' do
10
- tenant_id = 1234
11
- server.call(double, {'bogus' => 'message',
12
- 'multi_tenant' => { 'class' => MultiTenant.current_tenant_class, 'id' => tenant_id}},
14
+ server.call(double,{'bogus' => 'message',
15
+ 'multi_tenant' => { 'class' => account.class.name, 'id' => account.id}},
16
+ 'bogus_queue') do
17
+ expect(MultiTenant.current_tenant).to eq(account)
18
+ end
19
+ end
20
+
21
+ it 'sets the multitenant context (id) even if tenant not found' do
22
+ server.call(double,{'bogus' => 'message',
23
+ 'multi_tenant' => { 'class' => deleted_acount.class.name, 'id' => deleted_acount.id}},
13
24
  'bogus_queue') do
14
- expect(MultiTenant.current_tenant).to eq(tenant_id)
25
+ expect(MultiTenant.current_tenant).to eq(deleted_acount.id)
15
26
  end
16
27
  end
17
28
 
data/spec/schema.rb CHANGED
@@ -89,10 +89,21 @@ ARGV.grep(/\w+_spec\.rb/).empty? && ActiveRecord::Schema.define(version: 1) do
89
89
  t.column :category_id, :integer
90
90
  end
91
91
 
92
-
93
92
  create_table :allowed_places, force: true, id: false do |t|
94
- t.string :account_id, :integer
95
- t.string :name, :string
93
+ t.string :account_id, :integer
94
+ t.string :name, :string
95
+ end
96
+
97
+ create_table :domains, force: true, partition_key: :account_id do |t|
98
+ t.column :account_id, :integer
99
+ t.column :name, :string
100
+ t.column :deleted, :boolean, default: false
101
+ end
102
+
103
+ create_table :pages, force: true, partition_key: :account_id do |t|
104
+ t.column :account_id, :integer
105
+ t.column :name, :string
106
+ t.column :domain_id, :integer
96
107
  end
97
108
 
98
109
  create_distributed_table :accounts, :id
@@ -108,6 +119,8 @@ ARGV.grep(/\w+_spec\.rb/).empty? && ActiveRecord::Schema.define(version: 1) do
108
119
  create_distributed_table :uuid_records, :organization_id
109
120
  create_distributed_table :project_categories, :account_id
110
121
  create_distributed_table :allowed_places, :account_id
122
+ create_distributed_table :domains, :account_id
123
+ create_distributed_table :pages, :account_id
111
124
  create_reference_table :categories
112
125
  end
113
126
 
@@ -181,10 +194,7 @@ end
181
194
  class Comment < ActiveRecord::Base
182
195
  multi_tenant :account
183
196
  belongs_to :commentable, polymorphic: true
184
-
185
- if ActiveRecord::VERSION::MAJOR >= 4
186
- belongs_to :task, -> { where(comments: { commentable_type: 'Task' }) }, foreign_key: 'commentable_id'
187
- end
197
+ belongs_to :task, -> { where(comments: { commentable_type: 'Task' }) }, foreign_key: 'commentable_id'
188
198
  end
189
199
 
190
200
  class Organization < ActiveRecord::Base
@@ -207,7 +217,17 @@ class ProjectCategory < ActiveRecord::Base
207
217
  belongs_to :account
208
218
  end
209
219
 
210
-
211
220
  class AllowedPlace < ActiveRecord::Base
212
221
  multi_tenant :account
213
222
  end
223
+
224
+ class Domain < ActiveRecord::Base
225
+ multi_tenant :account
226
+ has_many :pages
227
+ default_scope { where(deleted: false) }
228
+ end
229
+
230
+ class Page < ActiveRecord::Base
231
+ multi_tenant :account
232
+ belongs_to :domain
233
+ end
data/spec/spec_helper.rb CHANGED
@@ -43,12 +43,7 @@ MultiTenantTest::Application.config.secret_token = 'x' * 40
43
43
  MultiTenantTest::Application.config.secret_key_base = 'y' * 40
44
44
 
45
45
  def uses_prepared_statements?
46
- if ActiveRecord::VERSION::MAJOR == 4
47
- config = ActiveRecord::Base.connection.instance_variable_get(:@config)
48
- config.fetch(:prepared_statements, true)
49
- else
50
- ActiveRecord::Base.connection.prepared_statements
51
- end
46
+ ActiveRecord::Base.connection.prepared_statements
52
47
  end
53
48
 
54
49
  require 'schema'