switchman 3.0.24 → 3.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 469ef6bca62776e9ba974a5c6d2a6220fc6ddcf1649956f43247465cbfb24f37
4
- data.tar.gz: 9fefef5095835cf28a6c72197ed74f6de9974d7c9c9019ad4ea87209d98c37e9
3
+ metadata.gz: eb62453808b659106e48a2279bdabeb2901c97187eaf8ae6cd612d460096f6d0
4
+ data.tar.gz: 5cf42b899e7a63f73bf5604247a8e2b2cd4a998bd7f4f0986180e451e66d3cc0
5
5
  SHA512:
6
- metadata.gz: 96d01726ff4e0e560e63964c5607417b7272202109b6e7e718769e9ba2cc04f82000f442c70aef8d6140e6dac00925197f0a95c70b790d47fbc2355d9021ae39
7
- data.tar.gz: 4b2edb3c0400f3280ba579bfaac61fce263ea5dd0881e5eeb77e675e0d2c1dfe04168e059f7bb91ac601ce2ef210c50c252945258fe2166ee6cf21c458defac0
6
+ metadata.gz: 69d3bb8a559d81a5cb5ef81c7c19395c3eab13a9a53a0bd920ea715b822cde6256a2464f317a6f719c04c8f9baf99b751b6abb07ae6edda40055cd53b32c5a30
7
+ data.tar.gz: e16d69bb4d2ec76fbe198a8d0d68e4217f3b2a261eff1fcde61b82357374f5c1b029e28f9ec852c090196461d2f45d162bc0c420182ccf0d3b7a4869f4ff5263
data/Rakefile CHANGED
@@ -35,4 +35,4 @@ RuboCop::RakeTask.new do |task|
35
35
  task.options = ['-S']
36
36
  end
37
37
 
38
- task default: %i[spec rubocop]
38
+ task default: %i[spec]
@@ -33,7 +33,7 @@ module Switchman
33
33
 
34
34
  protected
35
35
 
36
- def log(*args, &block)
36
+ def log(...)
37
37
  super
38
38
  ensure
39
39
  @last_query_at = Time.now
@@ -27,7 +27,7 @@ module Switchman
27
27
  shards = reflection.options[:multishard] && owner.respond_to?(:associated_shards) ? owner.associated_shards : [shard]
28
28
  # activate both the owner and the target's shard category, so that Reflection#join_id_for,
29
29
  # when called for the owner, will be returned relative to shard the query will execute on
30
- Shard.with_each_shard(shards, [klass.connection_classes, owner.class.connection_classes].uniq) do
30
+ Shard.with_each_shard(shards, [klass.connection_class_for_self, owner.class.connection_class_for_self].uniq) do
31
31
  super
32
32
  end
33
33
  end
@@ -83,10 +83,23 @@ module Switchman
83
83
 
84
84
  module Preloader
85
85
  module Association
86
+ module LoaderQuery
87
+ def load_records_in_batch(loaders)
88
+ # While in theory loading multiple associations that end up being effectively the same would be nice
89
+ # it's not very switchman compatible, so just don't bother trying to use that logic
90
+ # raw_records = records_for(loaders)
91
+
92
+ loaders.each do |loader|
93
+ loader.load_records(nil)
94
+ loader.run
95
+ end
96
+ end
97
+ end
98
+
86
99
  # Copypasta from Activerecord but with added global_id_for goodness.
87
100
  def records_for(ids)
88
101
  scope.where(association_key_name => ids).load do |record|
89
- global_key = if model.connection_classes == UnshardedRecord
102
+ global_key = if model.connection_class_for_self == UnshardedRecord
90
103
  convert_key(record[association_key_name])
91
104
  else
92
105
  Shard.global_id_for(record[association_key_name], record.shard)
@@ -100,13 +113,15 @@ module Switchman
100
113
  # significant changes:
101
114
  # * partition_by_shard the records_for call
102
115
  # * re-globalize the fetched owner id before looking up in the map
103
- def load_records
116
+ # TODO: the ignored param currently loads records; we should probably not waste effort double-loading them
117
+ # Change introduced here: https://github.com/rails/rails/commit/c6c0b2e8af64509b699b782aadfecaa430700ece
118
+ def load_records(raw_records = nil)
104
119
  # owners can be duplicated when a relation has a collection association join
105
120
  # #compare_by_identity makes such owners different hash keys
106
121
  @records_by_owner = {}.compare_by_identity
107
122
 
108
- if owner_keys.empty?
109
- raw_records = []
123
+ if ::Rails.version < '7.0' && owner_keys.empty?
124
+ raw_records ||= []
110
125
  else
111
126
  # determine the shard to search for each owner
112
127
  if reflection.macro == :belongs_to
@@ -123,12 +138,12 @@ module Switchman
123
138
  partition_proc = ->(owner) { owner.shard }
124
139
  end
125
140
 
126
- raw_records = Shard.partition_by_shard(owners, partition_proc) do |partitioned_owners|
141
+ raw_records ||= Shard.partition_by_shard(owners, partition_proc) do |partitioned_owners|
127
142
  relative_owner_keys = partitioned_owners.map do |owner|
128
143
  key = owner[owner_key_name]
129
144
  if key && owner.class.sharded_column?(owner_key_name)
130
145
  key = Shard.relative_id_for(key, owner.shard,
131
- Shard.current(klass.connection_classes))
146
+ Shard.current(klass.connection_class_for_self))
132
147
  end
133
148
  convert_key(key)
134
149
  end
@@ -200,7 +215,7 @@ module Switchman
200
215
  # this seems counter-intuitive, but the autosave code will assign to attribute bypassing switchman,
201
216
  # after reading the id attribute _without_ bypassing switchman. So we need Shard.current for the
202
217
  # category of the associated record to match Shard.current for the category of self
203
- shard.activate(connection_classes_for_reflection(reflection)) { super }
218
+ shard.activate(connection_class_for_self_for_reflection(reflection)) { super }
204
219
  end
205
220
  end
206
221
  end
@@ -25,6 +25,16 @@ module Switchman
25
25
  @sharded_column_values[column_name]
26
26
  end
27
27
 
28
+ def define_attribute_methods
29
+ super
30
+ # ensure that we're using the sharded attribute method
31
+ # and not the silly one in AR::AttributeMethods::PrimaryKey
32
+ return unless sharded_column?(@primary_key)
33
+
34
+ class_eval(build_sharded_getter('id', '_read_attribute(@primary_key)', "::#{connection_class_for_self.name}"), __FILE__, __LINE__)
35
+ class_eval(build_sharded_setter('id', @primary_key, "::#{connection_class_for_self.name}"), __FILE__, __LINE__)
36
+ end
37
+
28
38
  protected
29
39
 
30
40
  def reflection_for_integer_attribute(attr_name)
@@ -36,17 +46,30 @@ module Switchman
36
46
  raise if connection.open_transactions.positive?
37
47
  end
38
48
 
49
+ # rubocop:disable Naming/MethodParameterName
50
+ def define_cached_method(owner, name, namespace:, as:, &block)
51
+ if ::Rails.version < '7.0'
52
+ yield owner
53
+ owner.rename_method(as, name)
54
+ else
55
+ owner.define_cached_method(name, namespace: namespace, as: as, &block)
56
+ end
57
+ end
58
+ # rubocop:enable Naming/MethodParameterName
59
+
39
60
  def define_method_global_attribute(attr_name, owner:)
40
61
  if sharded_column?(attr_name)
41
- owner << <<-RUBY
42
- def global_#{attr_name}
43
- raw_value = original_#{attr_name}
44
- return nil if raw_value.nil?
45
- return raw_value if raw_value > ::Switchman::Shard::IDS_PER_SHARD
46
-
47
- ::Switchman::Shard.global_id_for(raw_value, shard)
48
- end
49
- RUBY
62
+ define_cached_method(owner, "global_#{attr_name}", as: "sharded_global_#{attr_name}", namespace: :switchman) do |batch|
63
+ batch << <<-RUBY
64
+ def sharded_global_#{attr_name}
65
+ raw_value = original_#{attr_name}
66
+ return nil if raw_value.nil?
67
+ return raw_value if raw_value > ::Switchman::Shard::IDS_PER_SHARD
68
+
69
+ ::Switchman::Shard.global_id_for(raw_value, shard)
70
+ end
71
+ RUBY
72
+ end
50
73
  else
51
74
  define_method_unsharded_column(attr_name, 'global', owner)
52
75
  end
@@ -54,113 +77,150 @@ module Switchman
54
77
 
55
78
  def define_method_local_attribute(attr_name, owner:)
56
79
  if sharded_column?(attr_name)
57
- owner << <<-RUBY
58
- def local_#{attr_name}
59
- raw_value = original_#{attr_name}
60
- return nil if raw_value.nil?
61
- return raw_value % ::Switchman::Shard::IDS_PER_SHARD
62
- end
63
- RUBY
80
+ define_cached_method(owner, "local_#{attr_name}", as: "sharded_local_#{attr_name}", namespace: :switchman) do |batch|
81
+ batch << <<-RUBY
82
+ def sharded_local_#{attr_name}
83
+ raw_value = original_#{attr_name}
84
+ return nil if raw_value.nil?
85
+ return raw_value % ::Switchman::Shard::IDS_PER_SHARD
86
+ end
87
+ RUBY
88
+ end
64
89
  else
65
90
  define_method_unsharded_column(attr_name, 'local', owner)
66
91
  end
67
92
  end
68
93
 
69
- # see also Base#connection_classes_for_reflection
94
+ # see also Base#connection_class_for_self_for_reflection
70
95
  # the difference being this will output static strings for the common cases, making them
71
96
  # more performant
72
- def connection_classes_code_for_reflection(reflection)
97
+ def connection_class_for_self_code_for_reflection(reflection)
73
98
  if reflection
74
99
  if reflection.options[:polymorphic]
75
100
  # a polymorphic association has to be discovered at runtime. This code ends up being something like
76
- # context_type.&.constantize&.connection_classes
77
- "read_attribute(:#{reflection.foreign_type})&.constantize&.connection_classes"
101
+ # context_type.&.constantize&.connection_class_for_self
102
+ "read_attribute(:#{reflection.foreign_type})&.constantize&.connection_class_for_self"
78
103
  else
79
104
  # otherwise we can just return a symbol for the statically known type of the association
80
- "::#{reflection.klass.connection_classes.name}"
105
+ "::#{reflection.klass.connection_class_for_self.name}"
81
106
  end
82
107
  else
83
- "::#{connection_classes.name}"
108
+ "::#{connection_class_for_self.name}"
84
109
  end
85
110
  end
86
111
 
87
- # just a dummy class with the proper interface that calls module_eval immediately
88
- class CodeGenerator
89
- def initialize(mod, line)
90
- @module = mod
91
- @line = line
112
+ def define_method_attribute(attr_name, owner:)
113
+ if sharded_column?(attr_name)
114
+ reflection = reflection_for_integer_attribute(attr_name)
115
+ class_name = connection_class_for_self_code_for_reflection(reflection)
116
+ safe_class_name = class_name.unpack1('h*')
117
+ define_cached_method(owner, attr_name, as: "sharded_#{safe_class_name}_#{attr_name}", namespace: :switchman) do |batch|
118
+ batch << build_sharded_getter("sharded_#{safe_class_name}_#{attr_name}", "original_#{attr_name}", class_name)
119
+ end
120
+ else
121
+ define_cached_method(owner, attr_name, as: "plain_#{attr_name}", namespace: :switchman) do |batch|
122
+ batch << <<-RUBY
123
+ def plain_#{attr_name}
124
+ _read_attribute("#{attr_name}") { |n| missing_attribute(n, caller) }
125
+ end
126
+ RUBY
127
+ end
92
128
  end
129
+ end
93
130
 
94
- def <<(string)
95
- @module.module_eval(string, __FILE__, @line)
96
- end
131
+ def build_sharded_getter(attr_name, raw_expr, attr_connection_class)
132
+ <<-RUBY
133
+ def #{attr_name}
134
+ raw_value = #{raw_expr}
135
+ return nil if raw_value.nil?
136
+
137
+ abs_raw_value = raw_value.abs
138
+ current_shard = ::Switchman::Shard.current(#{attr_connection_class})
139
+ same_shard = shard == current_shard
140
+ return raw_value if same_shard && abs_raw_value < ::Switchman::Shard::IDS_PER_SHARD
141
+
142
+ value_shard_id = abs_raw_value / ::Switchman::Shard::IDS_PER_SHARD
143
+ # this is a stupid case when someone stuffed a global id for the current shard in instead
144
+ # of a local id
145
+ return raw_value % ::Switchman::Shard::IDS_PER_SHARD if value_shard_id == current_shard.id
146
+ return raw_value if !same_shard && abs_raw_value > ::Switchman::Shard::IDS_PER_SHARD
147
+ return shard.global_id_for(raw_value) if !same_shard && abs_raw_value < ::Switchman::Shard::IDS_PER_SHARD
148
+
149
+ ::Switchman::Shard.relative_id_for(raw_value, shard, current_shard)
150
+ end
151
+ RUBY
97
152
  end
98
153
 
99
- def define_method_original_attribute(attr_name, owner:)
154
+ def define_method_attribute=(attr_name, owner:)
100
155
  if sharded_column?(attr_name)
101
156
  reflection = reflection_for_integer_attribute(attr_name)
102
- if attr_name == 'id'
103
- return if method_defined?(:original_id)
157
+ class_name = connection_class_for_self_code_for_reflection(reflection)
158
+ safe_class_name = class_name.unpack1('h*')
159
+ define_cached_method(owner, "#{attr_name}=", as: "sharded_#{safe_class_name}_#{attr_name}=", namespace: :switchman) do |batch|
160
+ batch << build_sharded_setter("sharded_#{safe_class_name}_#{attr_name}", attr_name, class_name)
161
+ end
162
+ else
163
+ define_cached_method(owner, "#{attr_name}=", as: "plain_#{attr_name}=", namespace: :switchman) do |batch|
164
+ batch << <<-RUBY
165
+ def plain_#{attr_name}=(new_value)
166
+ _write_attribute('#{attr_name}', new_value)
167
+ end
168
+ RUBY
169
+ end
170
+ end
171
+ end
104
172
 
105
- owner = CodeGenerator.new(self, __LINE__ + 4)
173
+ def build_sharded_setter(attr_name, attr_field, attr_connection_class)
174
+ <<-RUBY
175
+ def #{attr_name}=(new_value)
176
+ self.original_#{attr_field} = ::Switchman::Shard.relative_id_for(new_value, ::Switchman::Shard.current(#{attr_connection_class}), shard)
106
177
  end
178
+ RUBY
179
+ end
107
180
 
108
- owner << <<-RUBY
109
- # rename the original method to original_*
110
- alias_method 'original_#{attr_name}', '#{attr_name}'
111
- # and replace with one that transposes the id
112
- def #{attr_name}
113
- raw_value = original_#{attr_name}
114
- return nil if raw_value.nil?
115
-
116
- abs_raw_value = raw_value.abs
117
- current_shard = ::Switchman::Shard.current(#{connection_classes_code_for_reflection(reflection)})
118
- same_shard = shard == current_shard
119
- return raw_value if same_shard && abs_raw_value < ::Switchman::Shard::IDS_PER_SHARD
120
-
121
- value_shard_id = abs_raw_value / ::Switchman::Shard::IDS_PER_SHARD
122
- # this is a stupid case when someone stuffed a global id for the current shard in instead
123
- # of a local id
124
- return raw_value % ::Switchman::Shard::IDS_PER_SHARD if value_shard_id == current_shard.id
125
- return raw_value if !same_shard && abs_raw_value > ::Switchman::Shard::IDS_PER_SHARD
126
- return shard.global_id_for(raw_value) if !same_shard && abs_raw_value < ::Switchman::Shard::IDS_PER_SHARD
127
-
128
- ::Switchman::Shard.relative_id_for(raw_value, shard, current_shard)
129
- end
181
+ def define_method_original_attribute(attr_name, owner:)
182
+ if sharded_column?(attr_name)
183
+ define_cached_method(owner, "original_#{attr_name}", as: "sharded_original_#{attr_name}", namespace: :switchman) do |batch|
184
+ batch << <<-RUBY
185
+ def sharded_original_#{attr_name}
186
+ _read_attribute("#{attr_name}") { |n| missing_attribute(n, caller) }
187
+ end
188
+ RUBY
189
+ end
190
+ else
191
+ define_method_unsharded_column(attr_name, 'global', owner)
192
+ end
193
+ end
194
+
195
+ def define_method_original_attribute=(attr_name, owner:)
196
+ return unless sharded_column?(attr_name)
130
197
 
131
- alias_method 'original_#{attr_name}=', '#{attr_name}='
132
- def #{attr_name}=(new_value)
133
- self.original_#{attr_name} = ::Switchman::Shard.relative_id_for(new_value, ::Switchman::Shard.current(#{connection_classes_code_for_reflection(reflection)}), shard)
198
+ define_cached_method(owner, "original_#{attr_name}=", as: "sharded_original_#{attr_name}=", namespace: :switchman) do |batch|
199
+ batch << <<-RUBY
200
+ def sharded_original_#{attr_name}=(new_value)
201
+ _write_attribute('#{attr_name}', new_value)
134
202
  end
135
203
  RUBY
136
- else
137
- define_method_unsharded_column(attr_name, 'global', owner)
138
204
  end
139
205
  end
140
206
 
141
207
  def define_method_unsharded_column(attr_name, prefix, owner)
142
- return if columns_hash["#{prefix}_#{attr_name}"]
208
+ return if columns_hash["#{prefix}_#{attr_name}"] || attr_name == 'id'
143
209
 
144
- owner << <<-RUBY
145
- def #{prefix}_#{attr_name}
146
- raise NoMethodError, "undefined method `#{prefix}_#{attr_name}'; are you missing an association?"
147
- end
148
- RUBY
210
+ define_cached_method(owner, "#{prefix}_#{attr_name}", as: "unsharded_#{prefix}_#{attr_name}", namespace: :switchman) do |batch|
211
+ batch << <<-RUBY
212
+ def unsharded_#{prefix}_#{attr_name}
213
+ raise NoMethodError, "undefined method `#{prefix}_#{attr_name}'; are you missing an association?"
214
+ end
215
+ RUBY
216
+ end
149
217
  end
150
218
  end
151
219
 
152
- def self.included(klass)
153
- klass.singleton_class.include(ClassMethods)
220
+ def self.prepended(klass)
221
+ klass.singleton_class.prepend(ClassMethods)
154
222
  klass.attribute_method_prefix 'global_', 'local_', 'original_'
155
- end
156
-
157
- # ensure that we're using the sharded attribute method
158
- # and not the silly one in AR::AttributeMethods::PrimaryKey
159
- def id
160
- return super if is_a?(Shard)
161
-
162
- self.class.define_attribute_methods
163
- super
223
+ klass.attribute_method_affix prefix: 'original_', suffix: '='
164
224
  end
165
225
 
166
226
  # these are called if the specific methods haven't been defined yet
@@ -168,7 +228,7 @@ module Switchman
168
228
  return super unless self.class.sharded_column?(attr_name)
169
229
 
170
230
  reflection = self.class.send(:reflection_for_integer_attribute, attr_name)
171
- ::Switchman::Shard.relative_id_for(super, shard, ::Switchman::Shard.current(connection_classes_for_reflection(reflection)))
231
+ ::Switchman::Shard.relative_id_for(super, shard, ::Switchman::Shard.current(connection_class_for_self_for_reflection(reflection)))
172
232
  end
173
233
 
174
234
  def attribute=(attr_name, new_value)
@@ -178,7 +238,7 @@ module Switchman
178
238
  end
179
239
 
180
240
  reflection = self.class.send(:reflection_for_integer_attribute, attr_name)
181
- super(::Switchman::Shard.relative_id_for(new_value, ::Switchman::Shard.current(connection_classes_for_reflection(reflection)), shard))
241
+ super(::Switchman::Shard.relative_id_for(new_value, ::Switchman::Shard.current(connection_class_for_self_for_reflection(reflection)), shard))
182
242
  end
183
243
 
184
244
  def global_attribute(attr_name)
@@ -196,20 +256,6 @@ module Switchman
196
256
  attribute(attr_name)
197
257
  end
198
258
  end
199
-
200
- private
201
-
202
- def connection_classes_for_reflection(reflection)
203
- if reflection
204
- if reflection.options[:polymorphic]
205
- read_attribute(reflection.foreign_type)&.constantize&.connection_classes
206
- else
207
- reflection.klass.connection_classes
208
- end
209
- else
210
- self.class.connection_classes
211
- end
212
- end
213
259
  end
214
260
  end
215
261
  end
@@ -25,11 +25,11 @@ module Switchman
25
25
  def transaction(**)
26
26
  if self != ::ActiveRecord::Base && current_scope
27
27
  current_scope.activate do
28
- db = Shard.current(connection_classes).database_server
28
+ db = Shard.current(connection_class_for_self).database_server
29
29
  db.unguard { super }
30
30
  end
31
31
  else
32
- db = Shard.current(connection_classes).database_server
32
+ db = Shard.current(connection_class_for_self).database_server
33
33
  db.unguard { super }
34
34
  end
35
35
  end
@@ -78,7 +78,7 @@ module Switchman
78
78
  end
79
79
 
80
80
  def connected_to_stack
81
- return super if Thread.current.thread_variable?(:ar_connected_to_stack)
81
+ return super if ::Rails.version < '7.0' ? Thread.current.thread_variable?(:ar_connected_to_stack) : ::ActiveSupport::IsolatedExecutionState.key?(:active_record_connected_to_stack)
82
82
 
83
83
  ret = super
84
84
  DatabaseServer.guard_servers
@@ -92,7 +92,7 @@ module Switchman
92
92
  sharded_role = nil
93
93
  connected_to_stack.reverse_each do |hash|
94
94
  shard_role = hash.dig(:shard_roles, target_shard)
95
- if shard_role && (hash[:klasses].include?(::ActiveRecord::Base) || hash[:klasses].include?(connection_classes))
95
+ if shard_role && (hash[:klasses].include?(::ActiveRecord::Base) || hash[:klasses].include?(connection_class_for_self))
96
96
  sharded_role = shard_role
97
97
  break
98
98
  end
@@ -107,7 +107,7 @@ module Switchman
107
107
  # i.e. other sharded models don't inherit the current shard of Base
108
108
  def current_shard
109
109
  connected_to_stack.reverse_each do |hash|
110
- return hash[:shard] if hash[:shard] && hash[:klasses].include?(connection_classes)
110
+ return hash[:shard] if hash[:shard] && hash[:klasses].include?(connection_class_for_self)
111
111
  end
112
112
 
113
113
  default_shard
@@ -115,11 +115,17 @@ module Switchman
115
115
 
116
116
  def current_switchman_shard
117
117
  connected_to_stack.reverse_each do |hash|
118
- return hash[:switchman_shard] if hash[:switchman_shard] && hash[:klasses].include?(connection_classes)
118
+ return hash[:switchman_shard] if hash[:switchman_shard] && hash[:klasses].include?(connection_class_for_self)
119
119
  end
120
120
 
121
121
  Shard.default
122
122
  end
123
+
124
+ if ::Rails.version < '7.0'
125
+ def connection_class_for_self
126
+ connection_classes
127
+ end
128
+ end
123
129
  end
124
130
 
125
131
  def self.prepended(klass)
@@ -128,15 +134,39 @@ module Switchman
128
134
 
129
135
  def _run_initialize_callbacks
130
136
  @shard ||= if self.class.sharded_primary_key?
131
- Shard.shard_for(self[self.class.primary_key], Shard.current(self.class.connection_classes))
137
+ Shard.shard_for(self[self.class.primary_key], Shard.current(self.class.connection_class_for_self))
132
138
  else
133
- Shard.current(self.class.connection_classes)
139
+ Shard.current(self.class.connection_class_for_self)
134
140
  end
141
+ readonly! if shadow_record?
135
142
  super
136
143
  end
137
144
 
145
+ def shadow_record?
146
+ pkey = self[self.class.primary_key]
147
+ return false unless self.class.sharded_column?(self.class.primary_key) && pkey
148
+
149
+ pkey > Shard::IDS_PER_SHARD
150
+ end
151
+
152
+ def save_shadow_record(new_attrs: attributes, target_shard: Shard.current)
153
+ return if target_shard == shard
154
+
155
+ shadow_attrs = {}
156
+ new_attrs.each do |attr, value|
157
+ shadow_attrs[attr] = if self.class.sharded_column?(attr)
158
+ Shard.relative_id_for(value, shard, target_shard)
159
+ else
160
+ value
161
+ end
162
+ end
163
+ target_shard.activate do
164
+ self.class.upsert(shadow_attrs, unique_by: self.class.primary_key)
165
+ end
166
+ end
167
+
138
168
  def shard
139
- @shard || Shard.current(self.class.connection_classes) || Shard.default
169
+ @shard || Shard.current(self.class.connection_class_for_self) || Shard.default
140
170
  end
141
171
 
142
172
  def shard=(new_shard)
@@ -161,7 +191,7 @@ module Switchman
161
191
  end
162
192
 
163
193
  def destroy
164
- shard.activate(self.class.connection_classes) { super }
194
+ shard.activate(self.class.connection_class_for_self) { super }
165
195
  end
166
196
 
167
197
  def clone
@@ -174,14 +204,14 @@ module Switchman
174
204
  end
175
205
 
176
206
  def transaction(**kwargs, &block)
177
- shard.activate(self.class.connection_classes) do
207
+ shard.activate(self.class.connection_class_for_self) do
178
208
  self.class.transaction(**kwargs, &block)
179
209
  end
180
210
  end
181
211
 
182
212
  def with_transaction_returning_status
183
- shard.activate(self.class.connection_classes) do
184
- db = Shard.current(self.class.connection_classes).database_server
213
+ shard.activate(self.class.connection_class_for_self) do
214
+ db = Shard.current(self.class.connection_class_for_self).database_server
185
215
  db.unguard { super }
186
216
  end
187
217
  end
@@ -218,22 +248,22 @@ module Switchman
218
248
 
219
249
  protected
220
250
 
221
- # see also AttributeMethods#connection_classes_code_for_reflection
222
- def connection_classes_for_reflection(reflection)
251
+ # see also AttributeMethods#connection_class_for_self_code_for_reflection
252
+ def connection_class_for_self_for_reflection(reflection)
223
253
  if reflection
224
254
  if reflection.options[:polymorphic]
225
255
  begin
226
- read_attribute(reflection.foreign_type)&.constantize&.connection_classes || ::ActiveRecord::Base
256
+ read_attribute(reflection.foreign_type)&.constantize&.connection_class_for_self || ::ActiveRecord::Base
227
257
  rescue NameError
228
258
  # in case someone is abusing foreign_type to not point to an actual class
229
259
  ::ActiveRecord::Base
230
260
  end
231
261
  else
232
262
  # otherwise we can just return a symbol for the statically known type of the association
233
- reflection.klass.connection_classes
263
+ reflection.klass.connection_class_for_self
234
264
  end
235
265
  else
236
- self.class.connection_classes
266
+ self.class.connection_class_for_self
237
267
  end
238
268
  end
239
269
  end
@@ -4,7 +4,7 @@ module Switchman
4
4
  module ActiveRecord
5
5
  module Calculations
6
6
  def pluck(*column_names)
7
- target_shard = Shard.current(klass.connection_classes)
7
+ target_shard = Shard.current(klass.connection_class_for_self)
8
8
  shard_count = 0
9
9
  result = activate do |relation, shard|
10
10
  shard_count += 1
@@ -109,11 +109,18 @@ module Switchman
109
109
  private
110
110
 
111
111
  def type_cast_calculated_value_switchman(value, column_name, operation)
112
- type_cast_calculated_value(value, operation) do |val|
112
+ if ::Rails.version < '7.0'
113
+ type_cast_calculated_value(value, operation) do |val|
114
+ column = aggregate_column(column_name)
115
+ type ||= column.try(:type_caster) ||
116
+ lookup_cast_type_from_join_dependencies(column_name.to_s) || ::ActiveRecord::Type.default_value
117
+ type.deserialize(val)
118
+ end
119
+ else
113
120
  column = aggregate_column(column_name)
114
121
  type ||= column.try(:type_caster) ||
115
- lookup_cast_type_from_join_dependencies(column_name.to_s) || Type.default_value
116
- type.deserialize(val)
122
+ lookup_cast_type_from_join_dependencies(column_name.to_s) || ::ActiveRecord::Type.default_value
123
+ type_cast_calculated_value(value, operation, type)
117
124
  end
118
125
  end
119
126
 
@@ -39,7 +39,7 @@ module Switchman
39
39
  private
40
40
 
41
41
  def current_shard
42
- connection_klass.current_switchman_shard
42
+ ::Rails.version < '7.0' ? connection_klass.current_switchman_shard : connection_class.current_switchman_shard
43
43
  end
44
44
 
45
45
  def tls_key
@@ -7,7 +7,7 @@ module Switchman
7
7
  return super(id) unless klass.integral_id?
8
8
 
9
9
  if shard_source_value != :implicit
10
- current_shard = Shard.current(klass.connection_classes)
10
+ current_shard = Shard.current(klass.connection_class_for_self)
11
11
  result = activate do |relation, shard|
12
12
  current_id = Shard.relative_id_for(id, current_shard, shard)
13
13
  # current_id will be nil for non-integral id
@@ -33,7 +33,7 @@ module Switchman
33
33
  end
34
34
 
35
35
  def find_some_ordered(ids)
36
- current_shard = Shard.current(klass.connection_classes)
36
+ current_shard = Shard.current(klass.connection_class_for_self)
37
37
  ids = ids.map { |id| Shard.relative_id_for(id, current_shard, current_shard) }
38
38
  super(ids)
39
39
  end
@@ -20,13 +20,15 @@ module Switchman
20
20
  end
21
21
 
22
22
  module Migrator
23
- # significant change: just return MIGRATOR_SALT directly
24
- # especially if you're going through pgbouncer, the database
25
- # name you're accessing may not be consistent. it is NOT allowed
26
- # to run migrations against multiple shards in the same database
27
- # concurrently
23
+ # significant change: use the shard name instead of the database name
24
+ # in the lock id. Especially if you're going through pgbouncer, the
25
+ # database name you're accessing may not be consistent
28
26
  def generate_migrator_advisory_lock_id
29
- ::ActiveRecord::Migrator::MIGRATOR_SALT
27
+ db_name_hash = Zlib.crc32(Shard.current.name)
28
+ shard_name_hash = ::ActiveRecord::Migrator::MIGRATOR_SALT * db_name_hash
29
+ # Store in internalmetadata to allow other tools to be able to lock out migrations
30
+ ::ActiveRecord::InternalMetadata[:migrator_advisory_lock_id] = shard_name_hash
31
+ shard_name_hash
30
32
  end
31
33
 
32
34
  # significant change: strip out prefer_secondary from config
@@ -6,7 +6,7 @@ module Switchman
6
6
  module ClassMethods
7
7
  def quoted_table_name
8
8
  @quoted_table_name ||= {}
9
- @quoted_table_name[Shard.current(connection_classes).id] ||= connection.quote_table_name(table_name)
9
+ @quoted_table_name[Shard.current(connection_class_for_self).id] ||= connection.quote_table_name(table_name)
10
10
  end
11
11
  end
12
12
  end
@@ -5,11 +5,11 @@ module Switchman
5
5
  module Persistence
6
6
  # touch reads the id attribute directly, so it's not relative to the current shard
7
7
  def touch(*, **)
8
- shard.activate(self.class.connection_classes) { super }
8
+ shard.activate(self.class.connection_class_for_self) { super }
9
9
  end
10
10
 
11
11
  def update_columns(*)
12
- shard.activate(self.class.connection_classes) { super }
12
+ shard.activate(self.class.connection_class_for_self) { super }
13
13
  end
14
14
 
15
15
  def delete
@@ -7,7 +7,7 @@ module Switchman
7
7
  def create_database(name, options = {})
8
8
  options = { encoding: 'utf8' }.merge!(options.symbolize_keys)
9
9
 
10
- option_string = options.sum do |key, value|
10
+ option_string = options.sum('') do |key, value|
11
11
  case key
12
12
  when :owner
13
13
  " OWNER = \"#{value}\""
@@ -65,7 +65,7 @@ module Switchman
65
65
  when ::ActiveRecord::Relation
66
66
  Shard.default
67
67
  when nil
68
- Shard.current(klass.connection_classes)
68
+ Shard.current(klass.connection_class_for_self)
69
69
  else
70
70
  raise ArgumentError, "invalid shard value #{shard_value}"
71
71
  end
@@ -79,7 +79,7 @@ module Switchman
79
79
  when ::ActiveRecord::Base
80
80
  shard_value.respond_to?(:associated_shards) ? shard_value.associated_shards : [shard_value.shard]
81
81
  when nil
82
- [Shard.current(klass.connection_classes)]
82
+ [Shard.current(klass.connection_class_for_self)]
83
83
  else
84
84
  shard_value
85
85
  end
@@ -131,7 +131,7 @@ module Switchman
131
131
  id_shards = Set.new
132
132
  right.each do |value|
133
133
  local_id, id_shard = Shard.local_id_for(value)
134
- id_shard ||= Shard.current(klass.connection_classes) if local_id
134
+ id_shard ||= Shard.current(klass.connection_class_for_self) if local_id
135
135
  id_shards << id_shard if id_shard
136
136
  end
137
137
  return if id_shards.empty?
@@ -151,10 +151,13 @@ module Switchman
151
151
  end
152
152
  when ::Arel::Nodes::BindParam
153
153
  local_id, id_shard = Shard.local_id_for(right.value.value_before_type_cast)
154
- id_shard ||= Shard.current(klass.connection_classes) if local_id
154
+ id_shard ||= Shard.current(klass.connection_class_for_self) if local_id
155
+ when ::ActiveModel::Attribute
156
+ local_id, id_shard = Shard.local_id_for(right.value_before_type_cast)
157
+ id_shard ||= Shard.current(klass.connection_class_for_self) if local_id
155
158
  else
156
159
  local_id, id_shard = Shard.local_id_for(right)
157
- id_shard ||= Shard.current(klass.connection_classes) if local_id
160
+ id_shard ||= Shard.current(klass.connection_class_for_self) if local_id
158
161
  end
159
162
 
160
163
  return if !id_shard || id_shard == primary_shard
@@ -193,9 +196,9 @@ module Switchman
193
196
  reflection = model.send(:reflection_for_integer_attribute, column)
194
197
  break if reflection
195
198
  end
196
- return Shard.current(klass.connection_classes) if reflection.options[:polymorphic]
199
+ return Shard.current(klass.connection_class_for_self) if reflection.options[:polymorphic]
197
200
 
198
- Shard.current(reflection.klass.connection_classes)
201
+ Shard.current(reflection.klass.connection_class_for_self)
199
202
  end
200
203
 
201
204
  def relation_and_column(attribute)
@@ -291,7 +294,7 @@ module Switchman
291
294
  if source_shard
292
295
  source_shard
293
296
  elsif type == :primary
294
- Shard.current(klass.connection_classes)
297
+ Shard.current(klass.connection_class_for_self)
295
298
  elsif type == :foreign
296
299
  source_shard_for_foreign_key(relation, column)
297
300
  end
@@ -332,8 +335,9 @@ module Switchman
332
335
  end
333
336
 
334
337
  def transpose_predicate_value(value, current_shard, target_shard, attribute_type, remove_non_local_ids)
335
- if value.is_a?(::Arel::Nodes::BindParam)
336
- query_att = value.value
338
+ case value
339
+ when ::Arel::Nodes::BindParam, ::ActiveModel::Attribute
340
+ query_att = value.is_a?(::ActiveModel::Attribute) ? value : value.value
337
341
  current_id = query_att.value_before_type_cast
338
342
  if current_id.is_a?(::ActiveRecord::StatementCache::Substitute)
339
343
  current_id.sharded = true # mark for transposition later
@@ -346,7 +350,12 @@ module Switchman
346
350
  # make a new bind param
347
351
  value
348
352
  else
349
- ::Arel::Nodes::BindParam.new(query_att.class.new(query_att.name, local_id, query_att.type))
353
+ new_att = query_att.class.new(query_att.name, local_id, query_att.type)
354
+ if value.is_a?(::ActiveModel::Attribute)
355
+ new_att
356
+ else
357
+ ::Arel::Nodes::BindParam.new(new_att)
358
+ end
350
359
  end
351
360
  end
352
361
  else
@@ -5,7 +5,7 @@ module Switchman
5
5
  module Reflection
6
6
  module AbstractReflection
7
7
  def shard(owner)
8
- if polymorphic? || klass.connection_classes == owner.class.connection_classes
8
+ if polymorphic? || klass.connection_class_for_self == owner.class.connection_class_for_self
9
9
  # polymorphic associations assume the same shard as the owning item
10
10
  owner.shard
11
11
  else
@@ -9,13 +9,13 @@ module Switchman
9
9
 
10
10
  def initialize(*, **)
11
11
  super
12
- self.shard_value = Shard.current(klass ? klass.connection_classes : :primary) unless shard_value
12
+ self.shard_value = Shard.current(klass ? klass.connection_class_for_self : :primary) unless shard_value
13
13
  self.shard_source_value = :implicit unless shard_source_value
14
14
  end
15
15
 
16
16
  def clone
17
17
  result = super
18
- result.shard_value = Shard.current(klass ? klass.connection_classes : :primary) unless shard_value
18
+ result.shard_value = Shard.current(klass ? klass.connection_class_for_self : :primary) unless shard_value
19
19
  result
20
20
  end
21
21
 
@@ -29,35 +29,32 @@ module Switchman
29
29
  end
30
30
 
31
31
  def new(*, &block)
32
- primary_shard.activate(klass.connection_classes) { super }
32
+ primary_shard.activate(klass.connection_class_for_self) { super }
33
33
  end
34
34
 
35
35
  def create(*, &block)
36
- primary_shard.activate(klass.connection_classes) { super }
36
+ primary_shard.activate(klass.connection_class_for_self) { super }
37
37
  end
38
38
 
39
39
  def create!(*, &block)
40
- primary_shard.activate(klass.connection_classes) { super }
40
+ primary_shard.activate(klass.connection_class_for_self) { super }
41
41
  end
42
42
 
43
43
  def to_sql
44
- primary_shard.activate(klass.connection_classes) { super }
44
+ primary_shard.activate(klass.connection_class_for_self) { super }
45
45
  end
46
46
 
47
47
  def explain
48
48
  activate { |relation| relation.call_super(:explain, Relation) }
49
49
  end
50
50
 
51
- def records
52
- return @records if loaded?
53
-
54
- results = activate { |relation| relation.call_super(:records, Relation) }
55
- case shard_value
56
- when Array, ::ActiveRecord::Relation, ::ActiveRecord::Base
57
- @records = results
51
+ def load(&block)
52
+ if !loaded? || (::Rails.version >= '7.0' && scheduled?)
53
+ @records = activate { |relation| relation.send(:exec_queries, &block) }
58
54
  @loaded = true
59
55
  end
60
- results
56
+
57
+ self
61
58
  end
62
59
 
63
60
  %I[update_all delete_all].each do |method|
@@ -106,19 +103,19 @@ module Switchman
106
103
  def activate(unordered: false, &block)
107
104
  shards = all_shards
108
105
  if Array === shards && shards.length == 1
109
- if shards.first == DefaultShard || shards.first == Shard.current(klass.connection_classes)
106
+ if shards.first == DefaultShard || shards.first == Shard.current(klass.connection_class_for_self)
110
107
  yield(self, shards.first)
111
108
  else
112
- shards.first.activate(klass.connection_classes) { yield(self, shards.first) }
109
+ shards.first.activate(klass.connection_class_for_self) { yield(self, shards.first) }
113
110
  end
114
111
  else
115
112
  result_count = 0
116
113
  can_order = false
117
- result = Shard.with_each_shard(shards, [klass.connection_classes]) do
114
+ result = Shard.with_each_shard(shards, [klass.connection_class_for_self]) do
118
115
  # don't even query other shards if we're already past the limit
119
116
  next if limit_value && result_count >= limit_value && order_values.empty?
120
117
 
121
- relation = shard(Shard.current(klass.connection_classes), :to_a)
118
+ relation = shard(Shard.current(klass.connection_class_for_self), :to_a)
122
119
  # do a minimal query if possible
123
120
  relation = relation.limit(limit_value - result_count) if limit_value && !result_count.zero? && order_values.empty?
124
121
 
@@ -33,12 +33,12 @@ module Switchman
33
33
  primary_value = params[primary_index]
34
34
  target_shard = Shard.local_id_for(primary_value)[1]
35
35
  end
36
- current_shard = Shard.current(klass.connection_classes)
36
+ current_shard = Shard.current(klass.connection_class_for_self)
37
37
  target_shard ||= current_shard
38
38
 
39
39
  bind_values = bind_map.bind(params, current_shard, target_shard)
40
40
 
41
- target_shard.activate(klass.connection_classes) do
41
+ target_shard.activate(klass.connection_class_for_self) do
42
42
  sql = qualified_query_builder(target_shard, klass).sql_for(bind_values, connection)
43
43
  klass.find_by_sql(sql, bind_values)
44
44
  end
@@ -12,10 +12,14 @@ module Switchman
12
12
 
13
13
  # after :initialize_dependency_mechanism to ensure autoloading is configured for any downstream initializers that care
14
14
  # In rails 7.0 we should be able to just use an explicit after on configuring the once autoloaders and not need to go monkey around with initializer order
15
- initialize_dependency_mechanism = ::Rails::Application::Bootstrap.initializers.find { |i| i.name == :initialize_dependency_mechanism }
16
- initialize_dependency_mechanism.instance_variable_get(:@options)[:after] = :set_autoload_paths
15
+ if ::Rails.version < '7.0'
16
+ initialize_dependency_mechanism = ::Rails::Application::Bootstrap.initializers.find { |i| i.name == :initialize_dependency_mechanism }
17
+ initialize_dependency_mechanism.instance_variable_get(:@options)[:after] = :set_autoload_paths
18
+ end
17
19
 
18
- initializer 'switchman.active_record_patch', before: 'active_record.initialize_database', after: :initialize_dependency_mechanism do
20
+ initializer 'switchman.active_record_patch',
21
+ before: 'active_record.initialize_database',
22
+ after: (::Rails.version < '7.0' ? :initialize_dependency_mechanism : :setup_once_autoloader) do
19
23
  ::ActiveSupport.on_load(:active_record) do
20
24
  # Switchman requires postgres, so just always load the pg adapter
21
25
  require 'active_record/connection_adapters/postgresql_adapter'
@@ -24,7 +28,7 @@ module Switchman
24
28
  self.default_role = :primary
25
29
 
26
30
  prepend ActiveRecord::Base
27
- include ActiveRecord::AttributeMethods
31
+ prepend ActiveRecord::AttributeMethods
28
32
  include ActiveRecord::Persistence
29
33
  singleton_class.prepend ActiveRecord::ModelSchema::ClassMethods
30
34
 
@@ -46,6 +50,7 @@ module Switchman
46
50
  ::ActiveRecord::Associations::CollectionProxy.include(ActiveRecord::Associations::CollectionProxy)
47
51
 
48
52
  ::ActiveRecord::Associations::Preloader::Association.prepend(ActiveRecord::Associations::Preloader::Association)
53
+ ::ActiveRecord::Associations::Preloader::Association::LoaderQuery.prepend(ActiveRecord::Associations::Preloader::Association::LoaderQuery) unless ::Rails.version < '7.0'
49
54
  ::ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(ActiveRecord::AbstractAdapter)
50
55
  ::ActiveRecord::ConnectionAdapters::ConnectionPool.prepend(ActiveRecord::ConnectionPool)
51
56
  ::ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(ActiveRecord::QueryCache)
@@ -5,7 +5,7 @@ module Switchman
5
5
  module Relation
6
6
  def exec_queries(*args)
7
7
  if lock_value
8
- db = Shard.current(connection_classes).database_server
8
+ db = Shard.current(connection_class_for_self).database_server
9
9
  db.unguard { super }
10
10
  else
11
11
  super
@@ -15,7 +15,7 @@ module Switchman
15
15
  %w[update_all delete_all].each do |method|
16
16
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
17
17
  def #{method}(*args)
18
- db = Shard.current(connection_classes).database_server
18
+ db = Shard.current(connection_class_for_self).database_server
19
19
  db.unguard { super }
20
20
  end
21
21
  RUBY
@@ -16,7 +16,7 @@ module Switchman
16
16
  payload[:shard] = {
17
17
  database_server_id: shard.database_server.id,
18
18
  id: shard.id,
19
- env: @shard_host.pool.connection_klass&.current_role
19
+ env: ::Rails.version < '7.0' ? @shard_host.pool.connection_klass&.current_role : @shard_host.pool.connection_class&.current_role
20
20
  }
21
21
  end
22
22
  super name, payload
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Switchman
4
- VERSION = '3.0.24'
4
+ VERSION = '3.1.2'
5
5
  end
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: switchman
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.24
4
+ version: 3.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody Cutrer
8
8
  - James Williams
9
9
  - Jacob Fugal
10
- autorequire:
10
+ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2022-04-28 00:00:00.000000000 Z
13
+ date: 2022-07-19 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -21,7 +21,7 @@ dependencies:
21
21
  version: 6.1.4
22
22
  - - "<"
23
23
  - !ruby/object:Gem::Version
24
- version: '6.2'
24
+ version: '7.1'
25
25
  type: :runtime
26
26
  prerelease: false
27
27
  version_requirements: !ruby/object:Gem::Requirement
@@ -31,21 +31,21 @@ dependencies:
31
31
  version: 6.1.4
32
32
  - - "<"
33
33
  - !ruby/object:Gem::Version
34
- version: '6.2'
34
+ version: '7.1'
35
35
  - !ruby/object:Gem::Dependency
36
36
  name: guardrail
37
37
  requirement: !ruby/object:Gem::Requirement
38
38
  requirements:
39
39
  - - "~>"
40
40
  - !ruby/object:Gem::Version
41
- version: 3.0.0
41
+ version: 3.0.1
42
42
  type: :runtime
43
43
  prerelease: false
44
44
  version_requirements: !ruby/object:Gem::Requirement
45
45
  requirements:
46
46
  - - "~>"
47
47
  - !ruby/object:Gem::Version
48
- version: 3.0.0
48
+ version: 3.0.1
49
49
  - !ruby/object:Gem::Dependency
50
50
  name: parallel
51
51
  requirement: !ruby/object:Gem::Requirement
@@ -69,7 +69,7 @@ dependencies:
69
69
  version: '6.1'
70
70
  - - "<"
71
71
  - !ruby/object:Gem::Version
72
- version: '6.2'
72
+ version: '7.1'
73
73
  type: :runtime
74
74
  prerelease: false
75
75
  version_requirements: !ruby/object:Gem::Requirement
@@ -79,7 +79,7 @@ dependencies:
79
79
  version: '6.1'
80
80
  - - "<"
81
81
  - !ruby/object:Gem::Version
82
- version: '6.2'
82
+ version: '7.1'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: appraisal
85
85
  requirement: !ruby/object:Gem::Requirement
@@ -300,7 +300,7 @@ licenses:
300
300
  - MIT
301
301
  metadata:
302
302
  rubygems_mfa_required: 'true'
303
- post_install_message:
303
+ post_install_message:
304
304
  rdoc_options: []
305
305
  require_paths:
306
306
  - lib
@@ -308,15 +308,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
308
308
  requirements:
309
309
  - - ">="
310
310
  - !ruby/object:Gem::Version
311
- version: '2.6'
311
+ version: '2.7'
312
312
  required_rubygems_version: !ruby/object:Gem::Requirement
313
313
  requirements:
314
314
  - - ">="
315
315
  - !ruby/object:Gem::Version
316
316
  version: '0'
317
317
  requirements: []
318
- rubygems_version: 3.1.4
319
- signing_key:
318
+ rubygems_version: 3.1.6
319
+ signing_key:
320
320
  specification_version: 4
321
321
  summary: Rails sharding magic
322
322
  test_files: []