switchman 3.0.24 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 469ef6bca62776e9ba974a5c6d2a6220fc6ddcf1649956f43247465cbfb24f37
4
- data.tar.gz: 9fefef5095835cf28a6c72197ed74f6de9974d7c9c9019ad4ea87209d98c37e9
3
+ metadata.gz: 48b27124d5233adf585aed105d0b2522c76249bdd1e13fbcfd7e904241d3a624
4
+ data.tar.gz: fea1ccc0ad63f00633fb90bee1d7bd4606f6de0e18564250865c1b1dde61ef8d
5
5
  SHA512:
6
- metadata.gz: 96d01726ff4e0e560e63964c5607417b7272202109b6e7e718769e9ba2cc04f82000f442c70aef8d6140e6dac00925197f0a95c70b790d47fbc2355d9021ae39
7
- data.tar.gz: 4b2edb3c0400f3280ba579bfaac61fce263ea5dd0881e5eeb77e675e0d2c1dfe04168e059f7bb91ac601ce2ef210c50c252945258fe2166ee6cf21c458defac0
6
+ metadata.gz: c05b445111dd0e4b16ecbfaea6ab84557fa2cbadc6102fa0af6fb43cff2f6bbb6606e825f76ed20f6b8a4a2a2966e31babadf66c5eb065a6c4d8a7eea6a8b92a
7
+ data.tar.gz: 0af9e15706142f90772d24e2187db93ec83331997bccbb25c1b89cec2dde1bd2414d2774fec3785c7bea1a1b06d3fa5ad49e15b716f0aa35794e2e5734c848a8
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)
@@ -199,15 +259,15 @@ module Switchman
199
259
 
200
260
  private
201
261
 
202
- def connection_classes_for_reflection(reflection)
262
+ def connection_class_for_self_for_reflection(reflection)
203
263
  if reflection
204
264
  if reflection.options[:polymorphic]
205
- read_attribute(reflection.foreign_type)&.constantize&.connection_classes
265
+ read_attribute(reflection.foreign_type)&.constantize&.connection_class_for_self
206
266
  else
207
- reflection.klass.connection_classes
267
+ reflection.klass.connection_class_for_self
208
268
  end
209
269
  else
210
- self.class.connection_classes
270
+ self.class.connection_class_for_self
211
271
  end
212
272
  end
213
273
  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,15 @@ 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
135
141
  super
136
142
  end
137
143
 
138
144
  def shard
139
- @shard || Shard.current(self.class.connection_classes) || Shard.default
145
+ @shard || Shard.current(self.class.connection_class_for_self) || Shard.default
140
146
  end
141
147
 
142
148
  def shard=(new_shard)
@@ -161,7 +167,7 @@ module Switchman
161
167
  end
162
168
 
163
169
  def destroy
164
- shard.activate(self.class.connection_classes) { super }
170
+ shard.activate(self.class.connection_class_for_self) { super }
165
171
  end
166
172
 
167
173
  def clone
@@ -174,14 +180,14 @@ module Switchman
174
180
  end
175
181
 
176
182
  def transaction(**kwargs, &block)
177
- shard.activate(self.class.connection_classes) do
183
+ shard.activate(self.class.connection_class_for_self) do
178
184
  self.class.transaction(**kwargs, &block)
179
185
  end
180
186
  end
181
187
 
182
188
  def with_transaction_returning_status
183
- shard.activate(self.class.connection_classes) do
184
- db = Shard.current(self.class.connection_classes).database_server
189
+ shard.activate(self.class.connection_class_for_self) do
190
+ db = Shard.current(self.class.connection_class_for_self).database_server
185
191
  db.unguard { super }
186
192
  end
187
193
  end
@@ -218,22 +224,22 @@ module Switchman
218
224
 
219
225
  protected
220
226
 
221
- # see also AttributeMethods#connection_classes_code_for_reflection
222
- def connection_classes_for_reflection(reflection)
227
+ # see also AttributeMethods#connection_class_for_self_code_for_reflection
228
+ def connection_class_for_self_for_reflection(reflection)
223
229
  if reflection
224
230
  if reflection.options[:polymorphic]
225
231
  begin
226
- read_attribute(reflection.foreign_type)&.constantize&.connection_classes || ::ActiveRecord::Base
232
+ read_attribute(reflection.foreign_type)&.constantize&.connection_class_for_self || ::ActiveRecord::Base
227
233
  rescue NameError
228
234
  # in case someone is abusing foreign_type to not point to an actual class
229
235
  ::ActiveRecord::Base
230
236
  end
231
237
  else
232
238
  # otherwise we can just return a symbol for the statically known type of the association
233
- reflection.klass.connection_classes
239
+ reflection.klass.connection_class_for_self
234
240
  end
235
241
  else
236
- self.class.connection_classes
242
+ self.class.connection_class_for_self
237
243
  end
238
244
  end
239
245
  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
@@ -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.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
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.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody Cutrer
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2022-04-28 00:00:00.000000000 Z
13
+ date: 2022-06-02 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
@@ -308,14 +308,14 @@ 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
318
+ rubygems_version: 3.1.6
319
319
  signing_key:
320
320
  specification_version: 4
321
321
  summary: Rails sharding magic