switchman 3.0.24 → 4.2.4

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +16 -15
  3. data/db/migrate/20180828183945_add_default_shard_index.rb +1 -1
  4. data/db/migrate/20190114212900_add_unique_name_indexes.rb +10 -4
  5. data/lib/switchman/active_record/abstract_adapter.rb +11 -7
  6. data/lib/switchman/active_record/associations.rb +157 -50
  7. data/lib/switchman/active_record/attribute_methods.rb +192 -101
  8. data/lib/switchman/active_record/base.rb +136 -33
  9. data/lib/switchman/active_record/calculations.rb +91 -48
  10. data/lib/switchman/active_record/connection_handler.rb +18 -0
  11. data/lib/switchman/active_record/connection_pool.rb +41 -6
  12. data/lib/switchman/active_record/database_configurations.rb +23 -13
  13. data/lib/switchman/active_record/finder_methods.rb +22 -16
  14. data/lib/switchman/active_record/log_subscriber.rb +3 -6
  15. data/lib/switchman/active_record/migration.rb +42 -17
  16. data/lib/switchman/active_record/model_schema.rb +1 -1
  17. data/lib/switchman/active_record/pending_migration_connection.rb +17 -0
  18. data/lib/switchman/active_record/persistence.rb +32 -2
  19. data/lib/switchman/active_record/postgresql_adapter.rb +37 -22
  20. data/lib/switchman/active_record/predicate_builder.rb +2 -2
  21. data/lib/switchman/active_record/query_cache.rb +26 -17
  22. data/lib/switchman/active_record/query_methods.rb +249 -142
  23. data/lib/switchman/active_record/reflection.rb +10 -3
  24. data/lib/switchman/active_record/relation.rb +103 -32
  25. data/lib/switchman/active_record/spawn_methods.rb +3 -7
  26. data/lib/switchman/active_record/statement_cache.rb +13 -9
  27. data/lib/switchman/active_record/table_definition.rb +1 -1
  28. data/lib/switchman/active_record/tasks/database_tasks.rb +6 -1
  29. data/lib/switchman/active_record/test_fixtures.rb +71 -25
  30. data/lib/switchman/active_support/cache.rb +9 -4
  31. data/lib/switchman/arel.rb +16 -25
  32. data/lib/switchman/call_super.rb +2 -2
  33. data/lib/switchman/database_server.rb +68 -34
  34. data/lib/switchman/default_shard.rb +14 -3
  35. data/lib/switchman/engine.rb +36 -19
  36. data/lib/switchman/environment.rb +2 -2
  37. data/lib/switchman/errors.rb +13 -0
  38. data/lib/switchman/guard_rail/relation.rb +3 -3
  39. data/lib/switchman/parallel.rb +6 -6
  40. data/lib/switchman/r_spec_helper.rb +12 -11
  41. data/lib/switchman/shard.rb +182 -72
  42. data/lib/switchman/sharded_instrumenter.rb +9 -3
  43. data/lib/switchman/shared_schema_cache.rb +11 -0
  44. data/lib/switchman/standard_error.rb +4 -0
  45. data/lib/switchman/test_helper.rb +3 -3
  46. data/lib/switchman/version.rb +1 -1
  47. data/lib/switchman.rb +27 -15
  48. data/lib/tasks/switchman.rake +96 -60
  49. metadata +18 -168
@@ -25,6 +25,25 @@ module Switchman
25
25
  @sharded_column_values[column_name]
26
26
  end
27
27
 
28
+ def define_attribute_methods
29
+ result = super
30
+ # ensure that we're using the sharded attribute method
31
+ # and not the silly one in AR::AttributeMethods::PrimaryKey
32
+ return result unless sharded_column?(@primary_key)
33
+
34
+ class_eval(
35
+ build_sharded_getter("id",
36
+ "_read_attribute(@primary_key)",
37
+ "::#{connection_class_for_self.name}"),
38
+ __FILE__,
39
+ __LINE__
40
+ )
41
+ class_eval(build_sharded_setter("id", @primary_key, "::#{connection_class_for_self.name}"),
42
+ __FILE__,
43
+ __LINE__)
44
+ result
45
+ end
46
+
28
47
  protected
29
48
 
30
49
  def reflection_for_integer_attribute(attr_name)
@@ -36,131 +55,209 @@ module Switchman
36
55
  raise if connection.open_transactions.positive?
37
56
  end
38
57
 
39
- def define_method_global_attribute(attr_name, owner:)
40
- 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
58
+ def define_cached_method(owner, name, namespace:, as:, &)
59
+ if ::Rails.version < "7.1.4"
60
+ # https://github.com/rails/rails/commit/a2a12fc2e3f4e6d06f81d4c74c88f8e6b3369ee6#diff-5b59ece6d9396b596f06271cec0ea726e3360911383511c49b1a66f454bfc2b6L30
61
+ # These arguments were effectively swapped in Rails 7.1.4, so previous versions need them reversed
62
+ owner.define_cached_method(as, namespace:, as: name, &)
63
+ else
64
+ owner.define_cached_method(name, namespace:, as:, &)
65
+ end
66
+ end
46
67
 
47
- ::Switchman::Shard.global_id_for(raw_value, shard)
48
- end
49
- RUBY
68
+ def define_method_global_attribute(attr_name, owner:, as: attr_name)
69
+ if sharded_column?(attr_name)
70
+ define_cached_method(owner,
71
+ "sharded_global_#{attr_name}",
72
+ as: "global_#{as}",
73
+ namespace: :switchman) do |batch|
74
+ batch << <<-RUBY
75
+ def sharded_global_#{attr_name}
76
+ raw_value = original_#{attr_name}
77
+ return nil if raw_value.nil?
78
+ return raw_value if raw_value > ::Switchman::Shard::IDS_PER_SHARD
79
+
80
+ ::Switchman::Shard.global_id_for(raw_value, shard)
81
+ end
82
+ RUBY
83
+ end
50
84
  else
51
- define_method_unsharded_column(attr_name, 'global', owner)
85
+ define_method_unsharded_column(attr_name, "global", owner)
52
86
  end
53
87
  end
54
88
 
55
- def define_method_local_attribute(attr_name, owner:)
89
+ def define_method_local_attribute(attr_name, owner:, as: attr_name)
56
90
  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
91
+ define_cached_method(owner,
92
+ "sharded_local_#{attr_name}",
93
+ as: "local_#{as}",
94
+ namespace: :switchman) do |batch|
95
+ batch << <<-RUBY
96
+ def sharded_local_#{attr_name}
97
+ raw_value = original_#{attr_name}
98
+ return nil if raw_value.nil?
99
+ return raw_value % ::Switchman::Shard::IDS_PER_SHARD
100
+ end
101
+ RUBY
102
+ end
64
103
  else
65
- define_method_unsharded_column(attr_name, 'local', owner)
104
+ define_method_unsharded_column(attr_name, "local", owner)
66
105
  end
67
106
  end
68
107
 
69
- # see also Base#connection_classes_for_reflection
108
+ # see also Base#connection_class_for_self_for_reflection
70
109
  # the difference being this will output static strings for the common cases, making them
71
110
  # more performant
72
- def connection_classes_code_for_reflection(reflection)
111
+ def connection_class_for_self_code_for_reflection(reflection)
73
112
  if reflection
74
113
  if reflection.options[:polymorphic]
75
114
  # 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"
115
+ # context_type.&.constantize&.connection_class_for_self
116
+ <<~RUBY
117
+ begin
118
+ read_attribute(:#{reflection.foreign_type})&.constantize&.connection_class_for_self
119
+ rescue NameError
120
+ ::ActiveRecord::Base
121
+ end
122
+ RUBY
78
123
  else
79
124
  # otherwise we can just return a symbol for the statically known type of the association
80
- "::#{reflection.klass.connection_classes.name}"
125
+ "::#{reflection.klass.connection_class_for_self.name}"
81
126
  end
82
127
  else
83
- "::#{connection_classes.name}"
128
+ "::#{connection_class_for_self.name}"
84
129
  end
85
130
  end
86
131
 
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
132
+ def define_method_attribute(attr_name, owner:, as: attr_name)
133
+ if sharded_column?(attr_name)
134
+ reflection = reflection_for_integer_attribute(attr_name)
135
+ class_name = connection_class_for_self_code_for_reflection(reflection)
136
+ safe_class_name = class_name.unpack1("h*")
137
+ define_cached_method(owner,
138
+ "sharded_#{safe_class_name}_#{attr_name}",
139
+ as:,
140
+ namespace: :switchman) do |batch|
141
+ batch << build_sharded_getter("sharded_#{safe_class_name}_#{attr_name}",
142
+ "original_#{as}",
143
+ class_name)
144
+ end
145
+ else
146
+ define_cached_method(owner, "plain_#{attr_name}", as:, namespace: :switchman) do |batch|
147
+ batch << <<-RUBY
148
+ def plain_#{attr_name}
149
+ _read_attribute("#{attr_name}") { |n| missing_attribute(n, caller) }
150
+ end
151
+ RUBY
152
+ end
92
153
  end
154
+ end
93
155
 
94
- def <<(string)
95
- @module.module_eval(string, __FILE__, @line)
96
- end
156
+ def build_sharded_getter(attr_name, raw_expr, attr_connection_class)
157
+ <<-RUBY
158
+ def #{attr_name}
159
+ raw_value = #{raw_expr}
160
+ return nil if raw_value.nil?
161
+
162
+ abs_raw_value = raw_value.abs
163
+ current_shard = ::Switchman::Shard.current(#{attr_connection_class})
164
+ same_shard = loaded_from_shard == current_shard
165
+ return raw_value if same_shard && abs_raw_value < ::Switchman::Shard::IDS_PER_SHARD
166
+
167
+ value_shard_id = abs_raw_value / ::Switchman::Shard::IDS_PER_SHARD
168
+ # this is a stupid case when someone stuffed a global id for the current shard in instead
169
+ # of a local id
170
+ return raw_value % ::Switchman::Shard::IDS_PER_SHARD if value_shard_id == current_shard.id
171
+ return raw_value if !same_shard && abs_raw_value > ::Switchman::Shard::IDS_PER_SHARD
172
+ return loaded_from_shard.global_id_for(raw_value) if !same_shard && abs_raw_value < ::Switchman::Shard::IDS_PER_SHARD
173
+
174
+ ::Switchman::Shard.relative_id_for(raw_value, loaded_from_shard, current_shard)
175
+ end
176
+ RUBY
97
177
  end
98
178
 
99
- def define_method_original_attribute(attr_name, owner:)
179
+ def define_method_attribute=(attr_name, owner:, as: attr_name)
100
180
  if sharded_column?(attr_name)
101
181
  reflection = reflection_for_integer_attribute(attr_name)
102
- if attr_name == 'id'
103
- return if method_defined?(:original_id)
182
+ class_name = connection_class_for_self_code_for_reflection(reflection)
183
+ safe_class_name = class_name.unpack1("h*")
184
+ define_cached_method(owner,
185
+ "sharded_#{safe_class_name}_#{attr_name}=",
186
+ as: "#{as}=",
187
+ namespace: :switchman) do |batch|
188
+ batch << build_sharded_setter("sharded_#{safe_class_name}_#{attr_name}", attr_name, class_name)
189
+ end
190
+ else
191
+ define_cached_method(owner, "plain_#{attr_name}=", as: "#{as}=", namespace: :switchman) do |batch|
192
+ batch << <<-RUBY
193
+ def plain_#{attr_name}=(new_value)
194
+ _write_attribute('#{attr_name}', new_value)
195
+ end
196
+ RUBY
197
+ end
198
+ end
199
+ end
104
200
 
105
- owner = CodeGenerator.new(self, __LINE__ + 4)
201
+ def build_sharded_setter(attr_name, attr_field, attr_connection_class)
202
+ <<-RUBY
203
+ def #{attr_name}=(new_value)
204
+ self.original_#{attr_field} = ::Switchman::Shard.relative_id_for(new_value, ::Switchman::Shard.current(#{attr_connection_class}), loaded_from_shard)
106
205
  end
206
+ RUBY
207
+ end
107
208
 
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
209
+ def define_method_original_attribute(attr_name, owner:, as: attr_name)
210
+ if sharded_column?(attr_name)
211
+ define_cached_method(owner,
212
+ "sharded_original_#{attr_name}",
213
+ as: "original_#{as}",
214
+ namespace: :switchman) do |batch|
215
+ batch << <<-RUBY
216
+ def sharded_original_#{attr_name}
217
+ _read_attribute("#{attr_name}") { |n| missing_attribute(n, caller) }
218
+ end
219
+ RUBY
220
+ end
221
+ else
222
+ define_method_unsharded_column(attr_name, "global", owner)
223
+ end
224
+ end
225
+
226
+ def define_method_original_attribute=(attr_name, owner:, as: attr_name)
227
+ return unless sharded_column?(attr_name)
130
228
 
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)
229
+ define_cached_method(owner,
230
+ "sharded_original_#{attr_name}=",
231
+ as: "original_#{as}=",
232
+ namespace: :switchman) do |batch|
233
+ batch << <<-RUBY
234
+ def sharded_original_#{attr_name}=(new_value)
235
+ _write_attribute('#{attr_name}', new_value)
134
236
  end
135
237
  RUBY
136
- else
137
- define_method_unsharded_column(attr_name, 'global', owner)
138
238
  end
139
239
  end
140
240
 
141
241
  def define_method_unsharded_column(attr_name, prefix, owner)
142
- return if columns_hash["#{prefix}_#{attr_name}"]
143
-
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
242
+ return if columns_hash["#{prefix}_#{attr_name}"] || attr_name == "id"
243
+
244
+ define_cached_method(owner,
245
+ "unsharded_#{prefix}_#{attr_name}",
246
+ as: "#{prefix}_#{attr_name}",
247
+ namespace: :switchman) do |batch|
248
+ batch << <<-RUBY
249
+ def unsharded_#{prefix}_#{attr_name}
250
+ raise NoMethodError, "undefined method `#{prefix}_#{attr_name}'; are you missing an association?"
251
+ end
252
+ RUBY
253
+ end
149
254
  end
150
255
  end
151
256
 
152
- def self.included(klass)
153
- klass.singleton_class.include(ClassMethods)
154
- 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
257
+ def self.prepended(klass)
258
+ klass.singleton_class.prepend(ClassMethods)
259
+ klass.attribute_method_prefix "global_", "local_", "original_"
260
+ klass.attribute_method_affix prefix: "original_", suffix: "="
164
261
  end
165
262
 
166
263
  # these are called if the specific methods haven't been defined yet
@@ -168,7 +265,11 @@ module Switchman
168
265
  return super unless self.class.sharded_column?(attr_name)
169
266
 
170
267
  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)))
268
+ ::Switchman::Shard.relative_id_for(
269
+ super,
270
+ shard,
271
+ ::Switchman::Shard.current(connection_class_for_self_for_reflection(reflection))
272
+ )
172
273
  end
173
274
 
174
275
  def attribute=(attr_name, new_value)
@@ -178,7 +279,11 @@ module Switchman
178
279
  end
179
280
 
180
281
  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))
282
+ super(attr_name, ::Switchman::Shard.relative_id_for(
283
+ new_value,
284
+ ::Switchman::Shard.current(connection_class_for_self_for_reflection(reflection)),
285
+ shard
286
+ ))
182
287
  end
183
288
 
184
289
  def global_attribute(attr_name)
@@ -191,25 +296,11 @@ module Switchman
191
296
 
192
297
  def local_attribute(attr_name)
193
298
  if self.class.sharded_column?(attr_name)
194
- ::Switchman::Shard.local_id_for(attribute(attr_name), shard).first
299
+ ::Switchman::Shard.local_id_for(attribute(attr_name)).first
195
300
  else
196
301
  attribute(attr_name)
197
302
  end
198
303
  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
304
  end
214
305
  end
215
306
  end