switchman 3.0.14 → 3.5.20

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 (54) 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/action_controller/caching.rb +2 -2
  6. data/lib/switchman/active_record/abstract_adapter.rb +6 -6
  7. data/lib/switchman/active_record/associations.rb +365 -0
  8. data/lib/switchman/active_record/attribute_methods.rb +188 -99
  9. data/lib/switchman/active_record/base.rb +185 -40
  10. data/lib/switchman/active_record/calculations.rb +64 -40
  11. data/lib/switchman/active_record/connection_handler.rb +18 -0
  12. data/lib/switchman/active_record/connection_pool.rb +24 -5
  13. data/lib/switchman/active_record/database_configurations.rb +37 -13
  14. data/lib/switchman/active_record/finder_methods.rb +46 -16
  15. data/lib/switchman/active_record/log_subscriber.rb +11 -5
  16. data/lib/switchman/active_record/migration.rb +52 -8
  17. data/lib/switchman/active_record/model_schema.rb +1 -1
  18. data/lib/switchman/active_record/persistence.rb +31 -3
  19. data/lib/switchman/active_record/postgresql_adapter.rb +11 -10
  20. data/lib/switchman/active_record/predicate_builder.rb +2 -2
  21. data/lib/switchman/active_record/query_cache.rb +49 -20
  22. data/lib/switchman/active_record/query_methods.rb +187 -136
  23. data/lib/switchman/active_record/reflection.rb +1 -1
  24. data/lib/switchman/active_record/relation.rb +33 -26
  25. data/lib/switchman/active_record/spawn_methods.rb +2 -2
  26. data/lib/switchman/active_record/statement_cache.rb +11 -7
  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 +26 -16
  30. data/lib/switchman/active_support/cache.rb +20 -1
  31. data/lib/switchman/arel.rb +34 -18
  32. data/lib/switchman/call_super.rb +8 -2
  33. data/lib/switchman/database_server.rb +91 -45
  34. data/lib/switchman/default_shard.rb +14 -5
  35. data/lib/switchman/engine.rb +79 -126
  36. data/lib/switchman/environment.rb +2 -2
  37. data/lib/switchman/errors.rb +17 -2
  38. data/lib/switchman/guard_rail/relation.rb +8 -10
  39. data/lib/switchman/guard_rail.rb +5 -0
  40. data/lib/switchman/parallel.rb +68 -0
  41. data/lib/switchman/r_spec_helper.rb +14 -11
  42. data/lib/switchman/rails.rb +2 -5
  43. data/{app/models → lib}/switchman/shard.rb +186 -189
  44. data/lib/switchman/sharded_instrumenter.rb +5 -1
  45. data/lib/switchman/shared_schema_cache.rb +11 -0
  46. data/lib/switchman/standard_error.rb +6 -5
  47. data/lib/switchman/test_helper.rb +2 -2
  48. data/{app/models → lib}/switchman/unsharded_record.rb +1 -1
  49. data/lib/switchman/version.rb +1 -1
  50. data/lib/switchman.rb +44 -12
  51. data/lib/tasks/switchman.rake +74 -53
  52. metadata +42 -53
  53. data/lib/switchman/active_record/association.rb +0 -206
  54. data/lib/switchman/open4.rb +0 -80
@@ -12,8 +12,6 @@ module Switchman
12
12
  # :explicit - explicit set on the relation
13
13
  # :association - a special value that scopes from associations use to use slightly different logic
14
14
  # for foreign key transposition
15
- # :to_a - a special value that Relation#to_a uses when querying multiple shards to
16
- # remove primary keys from conditions that aren't applicable to the current shard
17
15
  def shard_value
18
16
  @values[:shard]
19
17
  end
@@ -44,10 +42,7 @@ module Switchman
44
42
  old_primary_shard = primary_shard
45
43
  self.shard_value = value
46
44
  self.shard_source_value = source
47
- if old_primary_shard != primary_shard || source == :to_a
48
- transpose_clauses(old_primary_shard, primary_shard,
49
- remove_nonlocal_primary_keys: source == :to_a)
50
- end
45
+ transpose_predicates(nil, old_primary_shard, primary_shard) if old_primary_shard != primary_shard
51
46
  self
52
47
  end
53
48
 
@@ -65,7 +60,7 @@ module Switchman
65
60
  when ::ActiveRecord::Relation
66
61
  Shard.default
67
62
  when nil
68
- Shard.current(klass.connection_classes)
63
+ Shard.current(klass.connection_class_for_self)
69
64
  else
70
65
  raise ArgumentError, "invalid shard value #{shard_value}"
71
66
  end
@@ -79,7 +74,7 @@ module Switchman
79
74
  when ::ActiveRecord::Base
80
75
  shard_value.respond_to?(:associated_shards) ? shard_value.associated_shards : [shard_value.shard]
81
76
  when nil
82
- [Shard.current(klass.connection_classes)]
77
+ [Shard.current(klass.connection_class_for_self)]
83
78
  else
84
79
  shard_value
85
80
  end
@@ -89,35 +84,37 @@ module Switchman
89
84
  super(other.shard(primary_shard))
90
85
  end
91
86
 
92
- private
87
+ # use a temp variable so that the new where clause is built before self.where_clause is read,
88
+ # since build_where_clause might mutate self.where_clause
89
+ def where!(opts, *rest)
90
+ new_clause = build_where_clause(opts, rest)
91
+ self.where_clause += new_clause
92
+ self
93
+ end
93
94
 
94
- %i[where having].each do |type|
95
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
96
- def transpose_#{type}_clauses(source_shard, target_shard, remove_nonlocal_primary_keys:)
97
- unless (predicates = #{type}_clause.send(:predicates)).empty?
98
- new_predicates = transpose_predicates(predicates, source_shard,
99
- target_shard, remove_nonlocal_primary_keys: remove_nonlocal_primary_keys)
100
- if new_predicates != predicates
101
- self.#{type}_clause = #{type}_clause.dup
102
- if new_predicates != predicates
103
- #{type}_clause.instance_variable_set(:@predicates, new_predicates)
104
- end
105
- end
106
- end
95
+ protected
96
+
97
+ def remove_nonlocal_primary_keys!
98
+ each_transposable_predicate_value do |value, predicate, _relation, _column, type|
99
+ next value unless
100
+ type == :primary &&
101
+ predicate.left.relation.klass == klass &&
102
+ (predicate.is_a?(::Arel::Nodes::Equality) || predicate.is_a?(::Arel::Nodes::HomogeneousIn))
103
+
104
+ (value.is_a?(Integer) && value > Shard::IDS_PER_SHARD) ? [] : value
107
105
  end
108
- RUBY
106
+ self
109
107
  end
110
108
 
111
- def transpose_clauses(source_shard, target_shard, remove_nonlocal_primary_keys: false)
112
- transpose_where_clauses(source_shard, target_shard, remove_nonlocal_primary_keys: remove_nonlocal_primary_keys)
113
- transpose_having_clauses(source_shard, target_shard, remove_nonlocal_primary_keys: remove_nonlocal_primary_keys)
114
- end
109
+ private
115
110
 
116
111
  def infer_shards_from_primary_key(predicates)
117
112
  return unless klass.integral_id?
118
113
 
119
114
  primary_key = predicates.detect do |predicate|
120
- (predicate.is_a?(::Arel::Nodes::Binary) || predicate.is_a?(::Arel::Nodes::HomogeneousIn)) &&
115
+ (predicate.is_a?(::Arel::Nodes::Equality) ||
116
+ predicate.is_a?(::Arel::Nodes::In) ||
117
+ predicate.is_a?(::Arel::Nodes::HomogeneousIn)) &&
121
118
  predicate.left.is_a?(::Arel::Attributes::Attribute) &&
122
119
  predicate.left.relation.is_a?(::Arel::Table) && predicate.left.relation.klass == klass &&
123
120
  klass.primary_key == predicate.left.name
@@ -131,7 +128,7 @@ module Switchman
131
128
  id_shards = Set.new
132
129
  right.each do |value|
133
130
  local_id, id_shard = Shard.local_id_for(value)
134
- id_shard ||= Shard.current(klass.connection_classes) if local_id
131
+ id_shard ||= Shard.current(klass.connection_class_for_self) if local_id
135
132
  id_shards << id_shard if id_shard
136
133
  end
137
134
  return if id_shards.empty?
@@ -145,21 +142,24 @@ module Switchman
145
142
  return
146
143
  else
147
144
  id_shards = id_shards.to_a
148
- transpose_clauses(primary_shard, id_shards.first)
145
+ transpose_predicates(nil, primary_shard, id_shards.first)
149
146
  self.shard_value = id_shards
150
147
  return
151
148
  end
152
149
  when ::Arel::Nodes::BindParam
153
150
  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
151
+ id_shard ||= Shard.current(klass.connection_class_for_self) if local_id
152
+ when ::ActiveModel::Attribute
153
+ local_id, id_shard = Shard.local_id_for(right.value_before_type_cast)
154
+ id_shard ||= Shard.current(klass.connection_class_for_self) if local_id
155
155
  else
156
156
  local_id, id_shard = Shard.local_id_for(right)
157
- id_shard ||= Shard.current(klass.connection_classes) if local_id
157
+ id_shard ||= Shard.current(klass.connection_class_for_self) if local_id
158
158
  end
159
159
 
160
160
  return if !id_shard || id_shard == primary_shard
161
161
 
162
- transpose_clauses(primary_shard, id_shard)
162
+ transpose_predicates(nil, primary_shard, id_shard)
163
163
  self.shard_value = id_shard
164
164
  end
165
165
 
@@ -177,25 +177,25 @@ module Switchman
177
177
  end
178
178
 
179
179
  def sharded_foreign_key?(relation, column)
180
- models_for_table(relation.table_name).any? { |m| m.sharded_column?(column) }
180
+ models_for_table(relation.name).any? { |m| m.sharded_column?(column) }
181
181
  end
182
182
 
183
183
  def sharded_primary_key?(relation, column)
184
184
  column = column.to_s
185
- return column == 'id' if relation.klass == ::ActiveRecord::Base
185
+ return column == "id" if relation.klass == ::ActiveRecord::Base
186
186
 
187
187
  relation.klass.primary_key == column && relation.klass.integral_id?
188
188
  end
189
189
 
190
190
  def source_shard_for_foreign_key(relation, column)
191
191
  reflection = nil
192
- models_for_table(relation.table_name).each do |model|
192
+ models_for_table(relation.name).each do |model|
193
193
  reflection = model.send(:reflection_for_integer_attribute, column)
194
194
  break if reflection
195
195
  end
196
- return Shard.current(klass.connection_classes) if reflection.options[:polymorphic]
196
+ return Shard.current(klass.connection_class_for_self) if reflection.options[:polymorphic]
197
197
 
198
- Shard.current(reflection.klass.connection_classes)
198
+ Shard.current(reflection.klass.connection_class_for_self)
199
199
  end
200
200
 
201
201
  def relation_and_column(attribute)
@@ -209,12 +209,11 @@ module Switchman
209
209
 
210
210
  case opts
211
211
  when String, Array
212
- values = Hash === rest.first ? rest.first.values : rest
212
+ values = (Hash === rest.first) ? rest.first.values : rest
213
213
 
214
- values.grep(ActiveRecord::Relation) do |rel|
215
- # serialize subqueries against the same shard as the outer query is currently
216
- # targeted to run against
217
- rel.shard!(primary_shard) if rel.shard_source_value == :implicit && rel.primary_shard != primary_shard
214
+ if shard_source_value != :explicit && values.grep(ActiveRecord::Relation).first
215
+ raise "Sub-queries are not allowed as simple substitutions; " \
216
+ "please build your relation with more structured methods so that Switchman is able to introspect it."
218
217
  end
219
218
 
220
219
  super
@@ -243,116 +242,168 @@ module Switchman
243
242
  connection.with_global_table_name { super }
244
243
  end
245
244
 
246
- def transpose_predicates(predicates,
247
- source_shard,
248
- target_shard,
249
- remove_nonlocal_primary_keys: false)
250
- predicates.map do |predicate|
251
- transpose_single_predicate(predicate, source_shard, target_shard,
252
- remove_nonlocal_primary_keys: remove_nonlocal_primary_keys)
253
- end
245
+ def each_predicate(predicates = nil, &block)
246
+ return predicates.map(&block) if predicates
247
+
248
+ each_predicate_cb(:having_clause, :having_clause=, &block)
249
+ each_predicate_cb(:where_clause, :where_clause=, &block)
254
250
  end
255
251
 
256
- def transpose_single_predicate(predicate,
257
- source_shard,
258
- target_shard,
259
- remove_nonlocal_primary_keys: false)
260
- if predicate.is_a?(::Arel::Nodes::Grouping)
261
- return predicate unless predicate.expr.is_a?(::Arel::Nodes::Or)
262
-
263
- # Dang, we have an OR. OK, that means we have other epxressions below this
264
- # level, perhaps many, that may need transposition.
265
- # the left side and right side must each be treated as predicate lists and
266
- # transformed in kind, if neither of them changes we can just return the grouping as is.
267
- # hold on, it's about to get recursive...
268
- or_expr = predicate.expr
269
- left_node = or_expr.left
270
- right_node = or_expr.right
271
- new_left_predicates = transpose_single_predicate(left_node, source_shard,
272
- target_shard, remove_nonlocal_primary_keys: remove_nonlocal_primary_keys)
273
- new_right_predicates = transpose_single_predicate(right_node, source_shard,
274
- target_shard, remove_nonlocal_primary_keys: remove_nonlocal_primary_keys)
275
- return predicate if new_left_predicates == left_node && new_right_predicates == right_node
276
-
277
- return ::Arel::Nodes::Grouping.new ::Arel::Nodes::Or.new(new_left_predicates, new_right_predicates)
278
- end
279
- return predicate unless predicate.is_a?(::Arel::Nodes::Binary) || predicate.is_a?(::Arel::Nodes::HomogeneousIn)
280
- return predicate unless predicate.left.is_a?(::Arel::Attributes::Attribute)
281
-
282
- relation, column = relation_and_column(predicate.left)
283
- return predicate unless (type = transposable_attribute_type(relation, column))
284
-
285
- remove = true if type == :primary &&
286
- remove_nonlocal_primary_keys &&
287
- predicate.left.relation.klass == klass &&
288
- (predicate.is_a?(::Arel::Nodes::Equality) || predicate.is_a?(::Arel::Nodes::HomogeneousIn))
289
-
290
- current_source_shard =
291
- if source_shard
292
- source_shard
293
- elsif type == :primary
294
- Shard.current(klass.connection_classes)
295
- elsif type == :foreign
296
- source_shard_for_foreign_key(relation, column)
297
- end
252
+ def each_predicate_cb(clause_getter, clause_setter, &block)
253
+ old_clause = send(clause_getter)
254
+ old_predicates = old_clause.send(:predicates)
255
+ return if old_predicates.empty?
298
256
 
299
- right = if predicate.is_a?(::Arel::Nodes::HomogeneousIn)
300
- predicate.values
301
- else
302
- predicate.right
303
- end
257
+ new_predicates = old_predicates.map(&block)
258
+ return if new_predicates == old_predicates
304
259
 
305
- new_right_value =
306
- case right
307
- when Array
308
- right.map { |val| transpose_predicate_value(val, current_source_shard, target_shard, type, remove).presence }.compact
309
- else
310
- transpose_predicate_value(right, current_source_shard, target_shard, type, remove)
260
+ new_clause = old_clause.dup
261
+ new_clause.instance_variable_set(:@predicates, new_predicates)
262
+
263
+ send(clause_setter, new_clause)
264
+ end
265
+
266
+ def each_transposable_predicate(predicates, &block)
267
+ each_predicate(predicates) do |predicate|
268
+ case predicate
269
+ when ::Arel::Nodes::Grouping
270
+ next predicate unless predicate.expr.is_a?(::Arel::Nodes::Or)
271
+
272
+ or_expr = predicate.expr
273
+ old_left = or_expr.left
274
+ old_right = or_expr.right
275
+ new_left, new_right = each_transposable_predicate([old_left, old_right], &block)
276
+
277
+ next predicate if new_left == old_left && new_right == old_right
278
+
279
+ next predicate.class.new predicate.expr.class.new(new_left, new_right)
280
+ when ::Arel::Nodes::SelectStatement
281
+ new_cores = predicate.cores.map do |core|
282
+ next core unless core.is_a?(::Arel::Nodes::SelectCore) # just in case something weird is going on
283
+
284
+ new_wheres = each_transposable_predicate(core.wheres, &block)
285
+ new_havings = each_transposable_predicate(core.havings, &block)
286
+
287
+ next core if core.wheres == new_wheres && core.havings == new_havings
288
+
289
+ new_core = core.clone
290
+ new_core.wheres = new_wheres
291
+ new_core.havings = new_havings
292
+ new_core
293
+ end
294
+
295
+ next predicate if predicate.cores == new_cores
296
+
297
+ new_node = predicate.clone
298
+ new_node.instance_variable_set(:@cores, new_cores)
299
+ next new_node
300
+ when ::Arel::Nodes::Not
301
+ old_value = predicate.expr
302
+ new_value = each_transposable_predicate([old_value], &block).first
303
+
304
+ next predicate if old_value == new_value
305
+
306
+ next predicate.class.new(new_value)
307
+ when ::Arel::Nodes::Exists
308
+ old_value = predicate.expressions
309
+ new_value = each_transposable_predicate([old_value], &block).first
310
+
311
+ next predicate if old_value == new_value
312
+
313
+ next predicate.class.new(new_value)
311
314
  end
312
315
 
313
- if new_right_value == right
314
- predicate
315
- elsif predicate.right.is_a?(::Arel::Nodes::Casted)
316
- if new_right_value == right.value
317
- predicate
318
- else
319
- predicate.class.new(predicate.left, right.class.new(new_right_value, right.attribute))
316
+ next predicate unless predicate.is_a?(::Arel::Nodes::Binary) || predicate.is_a?(::Arel::Nodes::HomogeneousIn)
317
+ next predicate unless predicate.left.is_a?(::Arel::Attributes::Attribute)
318
+
319
+ relation, column = relation_and_column(predicate.left)
320
+ next predicate unless (type = transposable_attribute_type(relation, column))
321
+
322
+ yield(predicate, relation, column, type)
323
+ end
324
+ end
325
+
326
+ def each_transposable_predicate_value(predicates = nil, &block)
327
+ each_transposable_predicate(predicates) do |predicate, relation, column, type|
328
+ each_transposable_predicate_value_cb(predicate, block) do |value|
329
+ yield(value, predicate, relation, column, type)
320
330
  end
321
- elsif predicate.is_a?(::Arel::Nodes::HomogeneousIn)
331
+ end
332
+ end
333
+
334
+ def each_transposable_predicate_value_cb(node, original_block, &block)
335
+ case node
336
+ when Array
337
+ node.filter_map { |val| each_transposable_predicate_value_cb(val, original_block, &block).presence }
338
+ when ::ActiveModel::Attribute
339
+ old_value = node.value_before_type_cast
340
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &block)
341
+
342
+ (old_value == new_value) ? node : node.class.new(node.name, new_value, node.type)
343
+ when ::Arel::Nodes::And
344
+ old_value = node.children
345
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &block)
346
+
347
+ (old_value == new_value) ? node : node.class.new(new_value)
348
+ when ::Arel::Nodes::BindParam
349
+ old_value = node.value
350
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &block)
351
+
352
+ (old_value == new_value) ? node : node.class.new(new_value)
353
+ when ::Arel::Nodes::Casted
354
+ old_value = node.value
355
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &block)
356
+
357
+ (old_value == new_value) ? node : node.class.new(new_value, node.attribute)
358
+ when ::Arel::Nodes::HomogeneousIn
359
+ old_value = node.values
360
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &block)
361
+
322
362
  # switch to a regular In, so that Relation::WhereClause#contradiction? knows about it
323
- if new_right_value.empty?
324
- klass = predicate.type == :in ? ::Arel::Nodes::In : ::Arel::Nodes::NotIn
325
- klass.new(predicate.attribute, new_right_value)
363
+ if new_value.empty?
364
+ klass = (node.type == :in) ? ::Arel::Nodes::In : ::Arel::Nodes::NotIn
365
+ klass.new(node.attribute, new_value)
326
366
  else
327
- predicate.class.new(new_right_value, predicate.attribute, predicate.type)
367
+ (old_value == new_value) ? node : node.class.new(new_value, node.attribute, node.type)
328
368
  end
369
+ when ::Arel::Nodes::Binary
370
+ old_value = node.right
371
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &block)
372
+
373
+ (old_value == new_value) ? node : node.class.new(node.left, new_value)
374
+ when ::Arel::Nodes::SelectStatement
375
+ each_transposable_predicate_value([node], &original_block).first
329
376
  else
330
- predicate.class.new(predicate.left, new_right_value)
377
+ yield(node)
331
378
  end
332
379
  end
333
380
 
334
- 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
337
- current_id = query_att.value_before_type_cast
338
- if current_id.is_a?(::ActiveRecord::StatementCache::Substitute)
339
- current_id.sharded = true # mark for transposition later
340
- current_id.primary = true if attribute_type == :primary
341
- value
342
- else
343
- local_id = Shard.relative_id_for(current_id, current_shard, target_shard) || current_id
344
- local_id = [] if remove_non_local_ids && local_id.is_a?(Integer) && local_id > Shard::IDS_PER_SHARD
345
- if current_id == local_id
346
- # make a new bind param
347
- value
381
+ def transpose_predicates(predicates,
382
+ source_shard,
383
+ target_shard)
384
+ each_transposable_predicate_value(predicates) do |value, _predicate, relation, column, type|
385
+ current_source_shard =
386
+ if source_shard
387
+ source_shard
388
+ elsif type == :primary
389
+ Shard.current(klass.connection_class_for_self)
390
+ elsif type == :foreign
391
+ source_shard_for_foreign_key(relation, column)
348
392
  else
349
- ::Arel::Nodes::BindParam.new(query_att.class.new(query_att.name, local_id, query_att.type))
393
+ primary_shard
350
394
  end
351
- end
395
+
396
+ transpose_predicate_value(value, current_source_shard, target_shard, type)
397
+ end
398
+ end
399
+
400
+ def transpose_predicate_value(value, current_shard, target_shard, attribute_type)
401
+ if value.is_a?(::ActiveRecord::StatementCache::Substitute)
402
+ value.sharded = true # mark for transposition later
403
+ value.primary = true if attribute_type == :primary
404
+ value
352
405
  else
353
- local_id = Shard.relative_id_for(value, current_shard, target_shard) || value
354
- local_id = [] if remove_non_local_ids && local_id.is_a?(Integer) && local_id > Shard::IDS_PER_SHARD
355
- local_id
406
+ Shard.relative_id_for(value, current_shard, target_shard) || value
356
407
  end
357
408
  end
358
409
  end
@@ -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
@@ -4,18 +4,18 @@ module Switchman
4
4
  module ActiveRecord
5
5
  module Relation
6
6
  def self.prepended(klass)
7
- klass::SINGLE_VALUE_METHODS.concat %i[shard shard_source]
7
+ klass::SINGLE_VALUE_METHODS.push(:shard, :shard_source)
8
8
  end
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,41 +29,39 @@ 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|
61
+ arg_params = (RUBY_VERSION <= "2.8") ? "*args" : "*args, **kwargs"
64
62
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
65
- def #{method}(*args)
66
- result = self.activate(unordered: true) { |relation| relation.call_super(#{method.inspect}, Relation, *args) }
63
+ def #{method}(#{arg_params})
64
+ result = self.activate(unordered: true) { |relation| relation.call_super(#{method.inspect}, Relation, #{arg_params}) }
67
65
  result = result.sum if result.is_a?(Array)
68
66
  result
69
67
  end
@@ -75,12 +73,16 @@ module Switchman
75
73
  loose_mode = options[:loose] && is_integer
76
74
  # loose_mode: if we don't care about getting exactly batch_size ids in between
77
75
  # don't get the max - just get the min and add batch_size so we get that many _at most_
78
- values = loose_mode ? 'MIN(id)' : 'MIN(id), MAX(id)'
76
+ values = loose_mode ? "MIN(id)" : "MIN(id), MAX(id)"
79
77
 
80
78
  batch_size = options[:batch_size].try(:to_i) || 1000
81
- quoted_primary_key = "#{klass.connection.quote_local_table_name(table_name)}.#{klass.connection.quote_column_name(primary_key)}"
82
- as_id = ' AS id' unless primary_key == 'id'
83
- subquery_scope = except(:select).select("#{quoted_primary_key}#{as_id}").reorder(primary_key.to_sym).limit(loose_mode ? 1 : batch_size)
79
+ quoted_primary_key =
80
+ "#{klass.connection.quote_local_table_name(table_name)}.#{klass.connection.quote_column_name(primary_key)}"
81
+ as_id = " AS id" unless primary_key == "id"
82
+ subquery_scope = except(:select)
83
+ .select("#{quoted_primary_key}#{as_id}")
84
+ .reorder(primary_key.to_sym)
85
+ .limit(loose_mode ? 1 : batch_size)
84
86
  subquery_scope = subquery_scope.where("#{quoted_primary_key} <= ?", options[:end_at]) if options[:end_at]
85
87
 
86
88
  first_subquery_scope = if options[:start_at]
@@ -106,21 +108,26 @@ module Switchman
106
108
  def activate(unordered: false, &block)
107
109
  shards = all_shards
108
110
  if Array === shards && shards.length == 1
109
- if shards.first == DefaultShard || shards.first == Shard.current(klass.connection_classes)
111
+ if !loaded? && shard_value != shards.first
112
+ shard(shards.first).activate(&block)
113
+ elsif shards.first == DefaultShard || shards.first == Shard.current(klass.connection_class_for_self)
110
114
  yield(self, shards.first)
111
115
  else
112
- shards.first.activate(klass.connection_classes) { yield(self, shards.first) }
116
+ shards.first.activate(klass.connection_class_for_self) { yield(self, shards.first) }
113
117
  end
114
118
  else
115
119
  result_count = 0
116
120
  can_order = false
117
- result = Shard.with_each_shard(shards, [klass.connection_classes]) do
121
+ result = Shard.with_each_shard(shards, [klass.connection_class_for_self]) do
118
122
  # don't even query other shards if we're already past the limit
119
123
  next if limit_value && result_count >= limit_value && order_values.empty?
120
124
 
121
- relation = shard(Shard.current(klass.connection_classes), :to_a)
125
+ relation = shard(Shard.current(klass.connection_class_for_self))
126
+ relation.remove_nonlocal_primary_keys!
122
127
  # do a minimal query if possible
123
- relation = relation.limit(limit_value - result_count) if limit_value && !result_count.zero? && order_values.empty?
128
+ if limit_value && !result_count.zero? && order_values.empty?
129
+ relation = relation.limit(limit_value - result_count)
130
+ end
124
131
 
125
132
  shard_results = relation.activate(&block)
126
133
 
@@ -17,7 +17,7 @@ module Switchman
17
17
  final_shard_source_value = %i[explicit association].detect do |source_value|
18
18
  shard_source_value == source_value || rhs.shard_source_value == source_value
19
19
  end
20
- raise 'unknown shard_source_value' unless final_shard_source_value
20
+ raise "unknown shard_source_value" unless final_shard_source_value
21
21
 
22
22
  # have to merge shard_value
23
23
  lhs_shard_value = all_shards
@@ -36,7 +36,7 @@ module Switchman
36
36
  final_shard_source_value = %i[explicit association implicit].detect do |source_value|
37
37
  shard_source_value == source_value || rhs.shard_source_value == source_value
38
38
  end
39
- raise 'unknown shard_source_value' unless final_shard_source_value
39
+ raise "unknown shard_source_value" unless final_shard_source_value
40
40
  end
41
41
 
42
42
  [final_shard_value, final_primary_shard, final_shard_source_value]
@@ -4,8 +4,8 @@ module Switchman
4
4
  module ActiveRecord
5
5
  module StatementCache
6
6
  module ClassMethods
7
- def create(connection, &block)
8
- relation = block.call ::ActiveRecord::StatementCache::Params.new
7
+ def create(connection)
8
+ relation = yield ::ActiveRecord::StatementCache::Params.new
9
9
 
10
10
  _query_builder, binds = connection.cacheable_query(self, relation.arel)
11
11
  bind_map = ::ActiveRecord::StatementCache::BindMap.new(binds)
@@ -25,7 +25,7 @@ module Switchman
25
25
  # we can make some assumptions about the shard source
26
26
  # (e.g. infer from the primary key or use the current shard)
27
27
 
28
- def execute(*args)
28
+ def execute(*args, &block)
29
29
  params, connection = args
30
30
  klass = @klass
31
31
  target_shard = nil
@@ -33,14 +33,14 @@ 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
- klass.find_by_sql(sql, bind_values)
43
+ klass.find_by_sql(sql, bind_values, &block)
44
44
  end
45
45
  end
46
46
 
@@ -66,7 +66,11 @@ module Switchman
66
66
 
67
67
  def primary_value_index
68
68
  primary_ba_index = @bound_attributes.index do |ba|
69
- ba.is_a?(::ActiveRecord::Relation::QueryAttribute) && ba.value.primary
69
+ if ba.value.is_a?(::ActiveRecord::StatementCache::Substitute)
70
+ ba.is_a?(::ActiveRecord::Relation::QueryAttribute) && ba.value.primary
71
+ else
72
+ false
73
+ end
70
74
  end
71
75
  @indexes.index(primary_ba_index) if primary_ba_index
72
76
  end
@@ -4,7 +4,7 @@ module Switchman
4
4
  module ActiveRecord
5
5
  module TableDefinition
6
6
  def column(name, type, limit: nil, **)
7
- Engine.foreign_key_check(name, type, limit: limit)
7
+ Switchman.foreign_key_check(name, type, limit: limit)
8
8
  super
9
9
  end
10
10
  end