activerecord-multi-tenant 1.0.4 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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'