switchman 1.5.21 → 2.1.5

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 (56) hide show
  1. checksums.yaml +5 -5
  2. data/app/models/switchman/shard.rb +757 -11
  3. data/db/migrate/20130328212039_create_switchman_shards.rb +3 -1
  4. data/db/migrate/20130328224244_create_default_shard.rb +4 -2
  5. data/db/migrate/20161206323434_add_back_default_string_limits_switchman.rb +13 -0
  6. data/db/migrate/20180828183945_add_default_shard_index.rb +15 -0
  7. data/db/migrate/20180828192111_add_timestamps_to_shards.rb +17 -0
  8. data/db/migrate/20190114212900_add_unique_name_indexes.rb +9 -0
  9. data/lib/switchman/action_controller/caching.rb +2 -0
  10. data/lib/switchman/active_record/abstract_adapter.rb +14 -4
  11. data/lib/switchman/active_record/association.rb +64 -37
  12. data/lib/switchman/active_record/attribute_methods.rb +54 -22
  13. data/lib/switchman/active_record/base.rb +76 -31
  14. data/lib/switchman/active_record/batches.rb +3 -1
  15. data/lib/switchman/active_record/calculations.rb +17 -22
  16. data/lib/switchman/active_record/connection_handler.rb +88 -78
  17. data/lib/switchman/active_record/connection_pool.rb +28 -23
  18. data/lib/switchman/active_record/finder_methods.rb +37 -28
  19. data/lib/switchman/active_record/log_subscriber.rb +14 -19
  20. data/lib/switchman/active_record/migration.rb +80 -0
  21. data/lib/switchman/active_record/model_schema.rb +3 -1
  22. data/lib/switchman/active_record/persistence.rb +9 -1
  23. data/lib/switchman/active_record/postgresql_adapter.rb +170 -126
  24. data/lib/switchman/active_record/predicate_builder.rb +3 -1
  25. data/lib/switchman/active_record/query_cache.rb +22 -87
  26. data/lib/switchman/active_record/query_methods.rb +139 -125
  27. data/lib/switchman/active_record/reflection.rb +42 -14
  28. data/lib/switchman/active_record/relation.rb +108 -33
  29. data/lib/switchman/active_record/spawn_methods.rb +2 -0
  30. data/lib/switchman/active_record/statement_cache.rb +44 -52
  31. data/lib/switchman/active_record/table_definition.rb +4 -2
  32. data/lib/switchman/active_record/type_caster.rb +2 -0
  33. data/lib/switchman/active_record/where_clause_factory.rb +5 -2
  34. data/lib/switchman/active_support/cache.rb +18 -0
  35. data/lib/switchman/arel.rb +8 -25
  36. data/lib/switchman/call_super.rb +19 -0
  37. data/lib/switchman/connection_pool_proxy.rb +70 -24
  38. data/lib/switchman/database_server.rb +69 -59
  39. data/lib/switchman/default_shard.rb +3 -0
  40. data/lib/switchman/engine.rb +44 -41
  41. data/lib/switchman/environment.rb +2 -0
  42. data/lib/switchman/errors.rb +2 -0
  43. data/lib/switchman/{shackles → guard_rail}/relation.rb +7 -5
  44. data/lib/switchman/{shackles.rb → guard_rail.rb} +6 -4
  45. data/lib/switchman/open4.rb +2 -0
  46. data/lib/switchman/r_spec_helper.rb +14 -8
  47. data/lib/switchman/rails.rb +2 -0
  48. data/lib/switchman/schema_cache.rb +17 -0
  49. data/lib/switchman/sharded_instrumenter.rb +4 -2
  50. data/lib/switchman/standard_error.rb +4 -2
  51. data/lib/switchman/test_helper.rb +7 -10
  52. data/lib/switchman/version.rb +3 -1
  53. data/lib/switchman.rb +5 -1
  54. data/lib/tasks/switchman.rake +53 -72
  55. metadata +84 -38
  56. data/app/models/switchman/shard_internal.rb +0 -692
@@ -1,4 +1,6 @@
1
- class CreateSwitchmanShards < ActiveRecord::Migration
1
+ # frozen_string_literal: true
2
+
3
+ class CreateSwitchmanShards < ActiveRecord::Migration[4.2]
2
4
  def change
3
5
  create_table :switchman_shards do |t|
4
6
  t.string :name
@@ -1,9 +1,11 @@
1
- class CreateDefaultShard < ActiveRecord::Migration
1
+ # frozen_string_literal: true
2
+
3
+ class CreateDefaultShard < ActiveRecord::Migration[4.2]
2
4
  def up
3
5
  unless Switchman::Shard.default.is_a?(Switchman::Shard)
4
6
  Switchman::Shard.reset_column_information
5
7
  Switchman::Shard.create!(:default => true)
6
- Switchman::Shard.default(true)
8
+ Switchman::Shard.default(reload: true)
7
9
  end
8
10
  end
9
11
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddBackDefaultStringLimitsSwitchman < ActiveRecord::Migration[4.2]
4
+ def up
5
+ add_string_limit_if_missing :switchman_shards, :name
6
+ add_string_limit_if_missing :switchman_shards, :database_server_id
7
+ end
8
+
9
+ def add_string_limit_if_missing(table, column)
10
+ return if column_exists?(table, column, :string, limit: 255)
11
+ change_column table, column, :string, limit: 255
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddDefaultShardIndex < ActiveRecord::Migration[4.2]
4
+ def change
5
+ Switchman::Shard.where(default: nil).update_all(default: false)
6
+ change_column_default :switchman_shards, :default, false
7
+ change_column_null :switchman_shards, :default, false
8
+ options = if connection.adapter_name == 'PostgreSQL'
9
+ { unique: true, where: "\"default\"" }
10
+ else
11
+ {}
12
+ end
13
+ add_index :switchman_shards, :default, **options
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddTimestampsToShards < ActiveRecord::Migration[4.2]
4
+ def change
5
+ add_timestamps :switchman_shards, null: true
6
+ now = Time.now.utc
7
+ Switchman::Shard.update_all(updated_at: now, created_at: now) if Switchman::Shard.current.default?
8
+ change_column_null :switchman_shards, :updated_at, false
9
+ change_column_null :switchman_shards, :created_at, false
10
+
11
+ if Switchman::Shard.current.default?
12
+ Switchman::Shard.connection.schema_cache.clear!
13
+ Switchman::Shard.reset_column_information
14
+ Switchman::Shard.columns
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddUniqueNameIndexes < ActiveRecord::Migration[4.2]
4
+ def change
5
+ add_index :switchman_shards, [:database_server_id, :name], unique: true
6
+ add_index :switchman_shards, :database_server_id, unique: true, where: "name IS NULL", name: 'index_switchman_shards_unique_primary_shard'
7
+ add_index :switchman_shards, "(true)", unique: true, where: "database_server_id IS NULL AND name IS NULL", name: 'index_switchman_shards_unique_primary_db_and_shard'
8
+ end
9
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Switchman
2
4
  module ActionController
3
5
  module Caching
@@ -1,11 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'switchman/sharded_instrumenter'
2
4
 
3
5
  module Switchman
4
6
  module ActiveRecord
5
7
  module AbstractAdapter
6
8
  module ForeignKeyCheck
7
- def add_column(table, name, type, options = {})
8
- Engine.foreign_key_check(name, type, options)
9
+ def add_column(table, name, type, limit: nil, **)
10
+ Engine.foreign_key_check(name, type, limit: limit)
9
11
  super
10
12
  end
11
13
  end
@@ -27,8 +29,8 @@ module Switchman
27
29
  quote_table_name(name)
28
30
  end
29
31
 
30
- def use_qualified_names?
31
- false
32
+ def schema_migration
33
+ ::ActiveRecord::SchemaMigration
32
34
  end
33
35
 
34
36
  protected
@@ -38,6 +40,14 @@ module Switchman
38
40
  ensure
39
41
  @last_query_at = Time.now
40
42
  end
43
+
44
+ private
45
+
46
+ def id_value_for_database(value)
47
+ return super unless value.class.sharded_primary_key?
48
+ # do this the Rails 4.2 way, so that if Shard.current != self.shard, the id gets transposed
49
+ quote(value.id)
50
+ end
41
51
  end
42
52
  end
43
53
  end
@@ -1,13 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Switchman
2
4
  module ActiveRecord
3
5
  module Association
4
6
  def shard
5
- if @reflection.options[:polymorphic] || @reflection.klass.shard_category == @owner.class.shard_category
6
- # polymorphic associations assume the same shard as the owning item
7
- @owner.shard
8
- else
9
- Shard.default
10
- end
7
+ reflection.shard(owner)
11
8
  end
12
9
 
13
10
  def build_record(*args)
@@ -18,12 +15,6 @@ module Switchman
18
15
  self.shard.activate { super }
19
16
  end
20
17
 
21
- def association_scope
22
- if klass
23
- shard.activate(klass.shard_category) { super }
24
- end
25
- end
26
-
27
18
  def scope
28
19
  shard_value = @reflection.options[:multishard] ? @owner : self.shard
29
20
  @owner.shard.activate { super.shard(shard_value, :association) }
@@ -41,9 +32,11 @@ module Switchman
41
32
  end
42
33
 
43
34
  module CollectionAssociation
44
- def get_records
45
- shards = reflection.options[:multishard] && owner.respond_to?(:associated_shards) ? owner.associated_shards : [owner.shard]
46
- Shard.with_each_shard(shards, [klass.shard_category]) do
35
+ def find_target
36
+ shards = reflection.options[:multishard] && owner.respond_to?(:associated_shards) ? owner.associated_shards : [shard]
37
+ # activate both the owner and the target's shard category, so that Reflection#join_id_for,
38
+ # when called for the owner, will be returned relative to shard the query will execute on
39
+ Shard.with_each_shard(shards, [klass.shard_category, owner.class.shard_category].uniq) do
47
40
  super
48
41
  end
49
42
  end
@@ -69,30 +62,49 @@ module Switchman
69
62
  end
70
63
  end
71
64
 
72
- if ::Rails.version >= '5'
73
- module Extension
74
- def self.build(_model, _reflection)
75
- end
76
-
77
- def self.valid_options
78
- [:multishard]
79
- end
65
+ module Extension
66
+ def self.build(_model, _reflection)
80
67
  end
81
68
 
82
- ::ActiveRecord::Associations::Builder::Association.extensions << Extension
83
- else
84
- module Builder
85
- module CollectionAssociation
86
- def valid_options
87
- super + [:multishard]
88
- end
89
- end
69
+ def self.valid_options
70
+ [:multishard]
90
71
  end
91
72
  end
92
73
 
74
+ ::ActiveRecord::Associations::Builder::Association.extensions << Extension
75
+
93
76
  module Preloader
94
77
  module Association
78
+ if ::Rails.version >= "5.2" and ::Rails.version < "6.0"
79
+ def run(preloader)
80
+ associated_records_by_owner.each do |owner, records|
81
+ associate_records_to_owner(owner, records)
82
+ end
83
+ end
84
+ end
85
+
86
+ if ::Rails.version >= "6.0"
87
+ # Copypasta from Activerecord but with added global_id_for goodness.
88
+ def records_for(ids)
89
+ scope.where(association_key_name => ids).load do |record|
90
+ global_key = if model.shard_category == :unsharded
91
+ convert_key(record[association_key_name])
92
+ else
93
+ Shard.global_id_for(record[association_key_name], record.shard)
94
+ end
95
+ owner = owners_by_key[global_key.to_s].first
96
+ association = owner.association(reflection.name)
97
+ association.set_inverse_instance(record)
98
+ end
99
+ end
100
+
101
+ def records_by_owner
102
+ associated_records_by_owner
103
+ end
104
+ end
105
+
95
106
  def associated_records_by_owner(preloader = nil)
107
+ return @associated_records_by_owner if defined?(@associated_records_by_owner)
96
108
  owners_map = owners_by_key
97
109
 
98
110
  if klass.nil? || owners_map.empty?
@@ -140,12 +152,13 @@ module Switchman
140
152
  records.flatten!
141
153
  end
142
154
 
143
- if ::Rails.version >= '4.1'
144
- @preloaded_records = records
145
- end
155
+ # This ivar may look unused, but remember this is an extension of
156
+ # rails' AR::Associations::Preloader::Association class. It gets used
157
+ # by that class (and its subclasses).
158
+ @preloaded_records = records
146
159
 
147
160
  # Each record may have multiple owners, and vice-versa
148
- records_by_owner = owners.each_with_object({}) do |owner,h|
161
+ @associated_records_by_owner = owners.each_with_object({}) do |owner,h|
149
162
  h[owner] = []
150
163
  end
151
164
  records.each do |record|
@@ -153,10 +166,11 @@ module Switchman
153
166
  owner_key = Shard.global_id_for(owner_key, record.shard) if owner_key && record.class.sharded_column?(association_key_name)
154
167
 
155
168
  owners_map[owner_key.to_s].each do |owner|
156
- records_by_owner[owner] << record
169
+ owner.association(reflection.name).set_inverse_instance(record)
170
+ @associated_records_by_owner[owner] << record
157
171
  end
158
172
  end
159
- records_by_owner
173
+ @associated_records_by_owner
160
174
  end
161
175
 
162
176
  def owners_by_key
@@ -174,6 +188,12 @@ module Switchman
174
188
  end
175
189
 
176
190
  module CollectionProxy
191
+ def initialize(*args)
192
+ super
193
+ self.shard_value = scope.shard_value
194
+ self.shard_source_value = :association
195
+ end
196
+
177
197
  def shard(*args)
178
198
  scope.shard(*args)
179
199
  end
@@ -185,6 +205,13 @@ module Switchman
185
205
  (record.has_attribute?(reflection.foreign_key) && record.send(reflection.foreign_key) != key) || # have to use send instead of [] because sharding
186
206
  record.attribute_changed?(reflection.foreign_key)
187
207
  end
208
+
209
+ def save_belongs_to_association(reflection)
210
+ # this seems counter-intuitive, but the autosave code will assign to attribute bypassing switchman,
211
+ # after reading the id attribute _without_ bypassing switchman. So we need Shard.current for the
212
+ # category of the associated record to match Shard.current for the category of self
213
+ shard.activate(shard_category_for_reflection(reflection)) { super }
214
+ end
188
215
  end
189
216
  end
190
217
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Switchman
2
4
  module ActiveRecord
3
5
  module AttributeMethods
@@ -27,7 +29,7 @@ module Switchman
27
29
  def reflection_for_integer_attribute(attr_name)
28
30
  attr_name = attr_name.to_s
29
31
  columns_hash[attr_name] && columns_hash[attr_name].type == :integer &&
30
- reflections.find { |_, r| r.belongs_to? && r.foreign_key.to_s == attr_name }.try(:last)
32
+ reflections.find { |_, r| r.belongs_to? && r.foreign_key.to_s == attr_name }&.last
31
33
  rescue ::ActiveRecord::StatementInvalid
32
34
  # this is for when models are referenced in initializers before migrations have been run
33
35
  raise if connection.open_transactions > 0
@@ -36,11 +38,15 @@ module Switchman
36
38
  def define_method_global_attribute(attr_name)
37
39
  if sharded_column?(attr_name)
38
40
  generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
39
- def __temp__
41
+ def __temp_global_attribute__
42
+ raw_value = original_#{attr_name}
43
+ return nil if raw_value.nil?
44
+ return raw_value if raw_value > Shard::IDS_PER_SHARD
45
+
40
46
  Shard.global_id_for(original_#{attr_name}, shard)
41
47
  end
42
- alias_method 'global_#{attr_name}', :__temp__
43
- undef_method :__temp__
48
+ alias_method 'global_#{attr_name}', :__temp_global_attribute__
49
+ undef_method :__temp_global_attribute__
44
50
  RUBY
45
51
  else
46
52
  define_method_unsharded_column(attr_name, 'global')
@@ -50,23 +56,28 @@ module Switchman
50
56
  def define_method_local_attribute(attr_name)
51
57
  if sharded_column?(attr_name)
52
58
  generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
53
- def __temp__
54
- Shard.local_id_for(original_#{attr_name}).first
59
+ def __temp_local_attribute__
60
+ raw_value = original_#{attr_name}
61
+ return nil if raw_value.nil?
62
+ return raw_value % Shard::IDS_PER_SHARD
55
63
  end
56
- alias_method 'local_#{attr_name}', :__temp__
57
- undef_method :__temp__
64
+ alias_method 'local_#{attr_name}', :__temp_local_attribute__
65
+ undef_method :__temp_local_attribute__
58
66
  RUBY
59
67
  else
60
68
  define_method_unsharded_column(attr_name, 'local')
61
69
  end
62
70
  end
63
71
 
72
+ # see also Base#shard_category_for_reflection
73
+ # the difference being this will output static strings for the common cases, making them
74
+ # more performant
64
75
  def shard_category_code_for_reflection(reflection)
65
76
  if reflection
66
77
  if reflection.options[:polymorphic]
67
78
  # a polymorphic association has to be discovered at runtime. This code ends up being something like
68
- # context_type.try(:constantize).try(:shard_category) || :default
69
- "read_attribute(:#{reflection.foreign_type}).try(:constantize).try(:shard_category) || :default"
79
+ # context_type.&.constantize&.shard_category || :primary
80
+ "read_attribute(:#{reflection.foreign_type})&.constantize&.shard_category || :primary"
70
81
  else
71
82
  # otherwise we can just return a symbol for the statically known type of the association
72
83
  reflection.klass.shard_category.inspect
@@ -79,22 +90,43 @@ module Switchman
79
90
  def define_method_original_attribute(attr_name)
80
91
  if sharded_column?(attr_name)
81
92
  reflection = reflection_for_integer_attribute(attr_name)
82
- generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
93
+ if attr_name == "id" && ::Rails.version >= '5.1.2'
94
+ return if self.method_defined?(:original_id)
95
+ owner = self
96
+ else
97
+ owner = generated_attribute_methods
98
+ end
99
+ owner.module_eval <<-RUBY, __FILE__, __LINE__ + 1
83
100
  # rename the original method to original_
84
101
  alias_method 'original_#{attr_name}', '#{attr_name}'
85
102
  # and replace with one that transposes the id
86
- def __temp__
87
- Shard.relative_id_for(original_#{attr_name}, shard, Shard.current(#{shard_category_code_for_reflection(reflection)}))
103
+ def __temp_relative_attribute__
104
+ raw_value = original_#{attr_name}
105
+ return nil if raw_value.nil?
106
+
107
+ abs_raw_value = raw_value.abs
108
+ current_shard = Shard.current(#{shard_category_code_for_reflection(reflection)})
109
+ same_shard = shard == current_shard
110
+ return raw_value if same_shard && abs_raw_value < Shard::IDS_PER_SHARD
111
+
112
+ value_shard_id = abs_raw_value / Shard::IDS_PER_SHARD
113
+ # this is a stupid case when someone stuffed a global id for the current shard in instead
114
+ # of a local id
115
+ return raw_value % Shard::IDS_PER_SHARD if value_shard_id == current_shard.id
116
+ return raw_value if !same_shard && abs_raw_value > Shard::IDS_PER_SHARD
117
+ return shard.global_id_for(raw_value) if !same_shard && abs_raw_value < Shard::IDS_PER_SHARD
118
+
119
+ Shard.relative_id_for(raw_value, shard, current_shard)
88
120
  end
89
- alias_method '#{attr_name}', :__temp__
90
- undef_method :__temp__
121
+ alias_method '#{attr_name}', :__temp_relative_attribute__
122
+ undef_method :__temp_relative_attribute__
91
123
 
92
124
  alias_method 'original_#{attr_name}=', '#{attr_name}='
93
- def __temp__(new_value)
125
+ def __temp_relative_attribute_assignment__(new_value)
94
126
  self.original_#{attr_name} = Shard.relative_id_for(new_value, Shard.current(#{shard_category_code_for_reflection(reflection)}), shard)
95
127
  end
96
- alias_method '#{attr_name}=', :__temp__
97
- undef_method :__temp__
128
+ alias_method '#{attr_name}=', :__temp_relative_attribute_assignment__
129
+ undef_method :__temp_relative_attribute_assignment__
98
130
  RUBY
99
131
  else
100
132
  define_method_unsharded_column(attr_name, 'global')
@@ -104,11 +136,11 @@ module Switchman
104
136
  def define_method_unsharded_column(attr_name, prefix)
105
137
  return if columns_hash["#{prefix}_#{attr_name}"]
106
138
  generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
107
- def __temp__
139
+ def __temp_unsharded_attribute__
108
140
  raise NoMethodError, "undefined method `#{prefix}_#{attr_name}'; are you missing an association?"
109
141
  end
110
- alias_method '#{prefix}_#{attr_name}', :__temp__
111
- undef_method :__temp__
142
+ alias_method '#{prefix}_#{attr_name}', :__temp_unsharded_attribute__
143
+ undef_method :__temp_unsharded_attribute__
112
144
  RUBY
113
145
  end
114
146
  end
@@ -117,7 +149,7 @@ module Switchman
117
149
  klass.extend(ClassMethods)
118
150
  klass.attribute_method_prefix "global_", "local_", "original_"
119
151
  end
120
-
152
+
121
153
  # ensure that we're using the sharded attribute method
122
154
  # and not the silly one in AR::AttributeMethods::PrimaryKey
123
155
  def id
@@ -1,11 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Switchman
2
4
  module ActiveRecord
3
5
  module Base
4
6
  module ClassMethods
5
7
  delegate :shard, to: :all
6
8
 
9
+ def find_ids_in_ranges(opts={}, &block)
10
+ opts.reverse_merge!(:loose => true)
11
+ all.find_ids_in_ranges(opts, &block)
12
+ end
13
+
7
14
  def shard_category
8
- @shard_category || (self.superclass < ::ActiveRecord::Base && self.superclass.shard_category) || :default
15
+ connection_specification_name.to_sym
9
16
  end
10
17
 
11
18
  def shard_category=(category)
@@ -14,34 +21,32 @@ module Switchman
14
21
  categories[shard_category].delete(self)
15
22
  categories.delete(shard_category) if categories[shard_category].empty?
16
23
  end
17
- connection_handler.uninitialize_ar(self)
18
24
  categories[category] ||= []
19
25
  categories[category] << self
20
- @shard_category = category
21
- connection_handler.initialize_categories(superclass)
26
+ self.connection_specification_name = category.to_s
22
27
  end
23
28
 
24
29
  def integral_id?
25
30
  if @integral_id == nil
26
- @integral_id = columns_hash[primary_key].try(:type) == :integer
31
+ @integral_id = columns_hash[primary_key]&.type == :integer
27
32
  end
28
33
  @integral_id
29
34
  end
30
35
 
31
- def transaction(*args)
36
+ def transaction(**)
32
37
  if self != ::ActiveRecord::Base && current_scope
33
38
  current_scope.activate do
34
39
  db = Shard.current(shard_category).database_server
35
- if ::Shackles.environment != db.shackles_environment
36
- db.unshackle { super }
40
+ if ::GuardRail.environment != db.guard_rail_environment
41
+ db.unguard { super }
37
42
  else
38
43
  super
39
44
  end
40
45
  end
41
46
  else
42
47
  db = Shard.current(shard_category).database_server
43
- if ::Shackles.environment != db.shackles_environment
44
- db.unshackle { super }
48
+ if ::GuardRail.environment != db.guard_rail_environment
49
+ db.unguard { super }
45
50
  else
46
51
  super
47
52
  end
@@ -65,21 +70,29 @@ module Switchman
65
70
  result
66
71
  end
67
72
  end
68
- end
69
73
 
70
- def self.included(klass)
71
- klass.extend(ClassMethods)
72
- klass.set_callback(:initialize, :before) do
73
- unless @shard
74
- if self.class.sharded_primary_key?
75
- @shard = Shard.shard_for(self[self.class.primary_key], Shard.current(self.class.shard_category))
76
- else
77
- @shard = Shard.current(self.class.shard_category)
74
+ def clear_query_caches_for_current_thread
75
+ ::ActiveRecord::Base.connection_handlers.each_value do |handler|
76
+ handler.connection_pool_list.each do |pool|
77
+ pool.connection(switch_shard: false).clear_query_cache if pool.active_connection?
78
78
  end
79
79
  end
80
80
  end
81
81
  end
82
82
 
83
+ def self.prepended(klass)
84
+ klass.singleton_class.prepend(ClassMethods)
85
+ end
86
+
87
+ def _run_initialize_callbacks
88
+ @shard ||= if self.class.sharded_primary_key?
89
+ Shard.shard_for(self[self.class.primary_key], Shard.current(self.class.shard_category))
90
+ else
91
+ Shard.current(self.class.shard_category)
92
+ end
93
+ super
94
+ end
95
+
83
96
  def shard
84
97
  @shard || Shard.current(self.class.shard_category) || Shard.default
85
98
  end
@@ -94,22 +107,18 @@ module Switchman
94
107
  end
95
108
  end
96
109
 
97
- def scope_class
98
- self.class.base_class
99
- end
100
-
101
- def save(*args)
110
+ def save(*, **)
102
111
  @shard_set_in_stone = true
103
- scope_class.shard(shard, :implicit).scoping { super }
112
+ (self.class.current_scope || self.class.default_scoped).shard(shard, :implicit).scoping { super }
104
113
  end
105
114
 
106
- def save!(*args)
115
+ def save!(*, **)
107
116
  @shard_set_in_stone = true
108
- scope_class.shard(shard, :implicit).scoping { super }
117
+ (self.class.current_scope || self.class.default_scoped).shard(shard, :implicit).scoping { super }
109
118
  end
110
119
 
111
120
  def destroy
112
- scope_class.shard(shard, :implicit).scoping { super }
121
+ self.class.shard(shard, :implicit).scoping { super }
113
122
  end
114
123
 
115
124
  def clone
@@ -121,14 +130,14 @@ module Switchman
121
130
  result
122
131
  end
123
132
 
124
- def transaction(options={}, &block)
133
+ def transaction(**kwargs, &block)
125
134
  shard.activate(self.class.shard_category) do
126
- self.class.transaction(options, &block)
135
+ self.class.transaction(**kwargs, &block)
127
136
  end
128
137
  end
129
138
 
130
139
  def hash
131
- self.class.sharded_primary_key? ? Shard.global_id_for(id).hash : super
140
+ self.class.sharded_primary_key? ? self.class.hash ^ Shard.global_id_for(id).hash : super
132
141
  end
133
142
 
134
143
  def to_param
@@ -141,6 +150,42 @@ module Switchman
141
150
  @shard_set_in_stone = false
142
151
  copy
143
152
  end
153
+
154
+ def quoted_id
155
+ return super unless self.class.sharded_primary_key?
156
+ # do this the Rails 4.2 way, so that if Shard.current != self.shard, the id gets transposed
157
+ self.class.connection.quote(id)
158
+ end
159
+
160
+ def update_columns(*)
161
+ db = shard.database_server
162
+ if ::GuardRail.environment != db.guard_rail_environment
163
+ return db.unguard { super }
164
+ else
165
+ super
166
+ end
167
+ end
168
+
169
+ protected
170
+
171
+ # see also AttributeMethods#shard_category_code_for_reflection
172
+ def shard_category_for_reflection(reflection)
173
+ if reflection
174
+ if reflection.options[:polymorphic]
175
+ begin
176
+ read_attribute(reflection.foreign_type)&.constantize&.shard_category || :primary
177
+ rescue NameError
178
+ # in case someone is abusing foreign_type to not point to an actual class
179
+ :primary
180
+ end
181
+ else
182
+ # otherwise we can just return a symbol for the statically known type of the association
183
+ reflection.klass.shard_category
184
+ end
185
+ else
186
+ shard_category
187
+ end
188
+ end
144
189
  end
145
190
  end
146
191
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Switchman
2
4
  module ActiveRecord
3
5
  module Batches
4
6
  def batch_order
5
- "#{connection.quote_local_table_name(table_name)}.#{quoted_primary_key} ASC"
7
+ ::Arel.sql("#{connection.quote_local_table_name(table_name)}.#{quoted_primary_key} ASC")
6
8
  end
7
9
  end
8
10
  end