switchman 3.4.1 → 4.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Rakefile +15 -14
- data/db/migrate/20180828183945_add_default_shard_index.rb +1 -1
- data/db/migrate/20190114212900_add_unique_name_indexes.rb +10 -4
- data/lib/switchman/active_record/abstract_adapter.rb +10 -6
- data/lib/switchman/active_record/associations.rb +71 -48
- data/lib/switchman/active_record/attribute_methods.rb +84 -37
- data/lib/switchman/active_record/base.rb +72 -41
- data/lib/switchman/active_record/calculations.rb +90 -54
- data/lib/switchman/active_record/connection_handler.rb +18 -0
- data/lib/switchman/active_record/connection_pool.rb +41 -23
- data/lib/switchman/active_record/database_configurations.rb +23 -13
- data/lib/switchman/active_record/finder_methods.rb +20 -14
- data/lib/switchman/active_record/log_subscriber.rb +3 -6
- data/lib/switchman/active_record/migration.rb +19 -19
- data/lib/switchman/active_record/pending_migration_connection.rb +17 -0
- data/lib/switchman/active_record/persistence.rb +22 -0
- data/lib/switchman/active_record/postgresql_adapter.rb +37 -22
- data/lib/switchman/active_record/predicate_builder.rb +2 -2
- data/lib/switchman/active_record/query_cache.rb +26 -17
- data/lib/switchman/active_record/query_methods.rb +148 -44
- data/lib/switchman/active_record/reflection.rb +9 -2
- data/lib/switchman/active_record/relation.rb +86 -16
- data/lib/switchman/active_record/spawn_methods.rb +3 -7
- data/lib/switchman/active_record/statement_cache.rb +4 -4
- data/lib/switchman/active_record/table_definition.rb +1 -1
- data/lib/switchman/active_record/tasks/database_tasks.rb +6 -1
- data/lib/switchman/active_record/test_fixtures.rb +71 -25
- data/lib/switchman/active_support/cache.rb +9 -4
- data/lib/switchman/arel.rb +16 -25
- data/lib/switchman/call_super.rb +2 -8
- data/lib/switchman/database_server.rb +67 -24
- data/lib/switchman/default_shard.rb +14 -3
- data/lib/switchman/engine.rb +35 -23
- data/lib/switchman/environment.rb +2 -2
- data/lib/switchman/errors.rb +4 -1
- data/lib/switchman/guard_rail/relation.rb +1 -2
- data/lib/switchman/parallel.rb +5 -5
- data/lib/switchman/r_spec_helper.rb +12 -11
- data/lib/switchman/shard.rb +168 -68
- data/lib/switchman/sharded_instrumenter.rb +9 -3
- data/lib/switchman/standard_error.rb +4 -0
- data/lib/switchman/test_helper.rb +3 -3
- data/lib/switchman/version.rb +1 -1
- data/lib/switchman.rb +27 -15
- data/lib/tasks/switchman.rake +96 -60
- metadata +28 -187
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
module Switchman
|
|
4
4
|
module ActiveRecord
|
|
5
5
|
module QueryMethods
|
|
6
|
+
# Use this class to prevent a value from getting transposed across shards
|
|
7
|
+
class NonTransposingValue < SimpleDelegator
|
|
8
|
+
def class
|
|
9
|
+
__getobj__.class
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def is_a?(other)
|
|
13
|
+
return true if other == NonTransposingValue
|
|
14
|
+
|
|
15
|
+
__getobj__.is_a?(other)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
6
19
|
# shard_value is one of:
|
|
7
20
|
# A shard
|
|
8
21
|
# An array or relation of shards
|
|
@@ -21,13 +34,29 @@ module Switchman
|
|
|
21
34
|
end
|
|
22
35
|
|
|
23
36
|
def shard_value=(value)
|
|
24
|
-
|
|
37
|
+
if @loaded
|
|
38
|
+
error_class = if ::Rails.version < "7.2"
|
|
39
|
+
::ActiveRecord::ImmutableRelation
|
|
40
|
+
else
|
|
41
|
+
::ActiveRecord::UnmodifiableRelation
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
raise error_class
|
|
45
|
+
end
|
|
25
46
|
|
|
26
47
|
@values[:shard] = value
|
|
27
48
|
end
|
|
28
49
|
|
|
29
50
|
def shard_source_value=(value)
|
|
30
|
-
|
|
51
|
+
if @loaded
|
|
52
|
+
error_class = if ::Rails.version < "7.2"
|
|
53
|
+
::ActiveRecord::ImmutableRelation
|
|
54
|
+
else
|
|
55
|
+
::ActiveRecord::UnmodifiableRelation
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
raise error_class
|
|
59
|
+
end
|
|
31
60
|
|
|
32
61
|
@values[:shard_source] = value
|
|
33
62
|
end
|
|
@@ -84,8 +113,20 @@ module Switchman
|
|
|
84
113
|
super(other.shard(primary_shard))
|
|
85
114
|
end
|
|
86
115
|
|
|
116
|
+
# use a temp variable so that the new where clause is built before self.where_clause is read,
|
|
117
|
+
# since build_where_clause might mutate self.where_clause
|
|
118
|
+
def where!(opts, *rest)
|
|
119
|
+
new_clause = build_where_clause(opts, rest)
|
|
120
|
+
self.where_clause += new_clause
|
|
121
|
+
self
|
|
122
|
+
end
|
|
123
|
+
|
|
87
124
|
protected
|
|
88
125
|
|
|
126
|
+
def arel_columns(columns)
|
|
127
|
+
connection.with_local_table_name { super }
|
|
128
|
+
end
|
|
129
|
+
|
|
89
130
|
def remove_nonlocal_primary_keys!
|
|
90
131
|
each_transposable_predicate_value do |value, predicate, _relation, _column, type|
|
|
91
132
|
next value unless
|
|
@@ -93,7 +134,7 @@ module Switchman
|
|
|
93
134
|
predicate.left.relation.klass == klass &&
|
|
94
135
|
(predicate.is_a?(::Arel::Nodes::Equality) || predicate.is_a?(::Arel::Nodes::HomogeneousIn))
|
|
95
136
|
|
|
96
|
-
value.is_a?(Integer) && value > Shard::IDS_PER_SHARD ? [] : value
|
|
137
|
+
(value.is_a?(Integer) && value > Shard::IDS_PER_SHARD) ? [] : value
|
|
97
138
|
end
|
|
98
139
|
self
|
|
99
140
|
end
|
|
@@ -104,7 +145,9 @@ module Switchman
|
|
|
104
145
|
return unless klass.integral_id?
|
|
105
146
|
|
|
106
147
|
primary_key = predicates.detect do |predicate|
|
|
107
|
-
(predicate.is_a?(::Arel::Nodes::
|
|
148
|
+
(predicate.is_a?(::Arel::Nodes::Equality) ||
|
|
149
|
+
predicate.is_a?(::Arel::Nodes::In) ||
|
|
150
|
+
predicate.is_a?(::Arel::Nodes::HomogeneousIn)) &&
|
|
108
151
|
predicate.left.is_a?(::Arel::Attributes::Attribute) &&
|
|
109
152
|
predicate.left.relation.is_a?(::Arel::Table) && predicate.left.relation.klass == klass &&
|
|
110
153
|
klass.primary_key == predicate.left.name
|
|
@@ -167,19 +210,19 @@ module Switchman
|
|
|
167
210
|
end
|
|
168
211
|
|
|
169
212
|
def sharded_foreign_key?(relation, column)
|
|
170
|
-
models_for_table(relation.
|
|
213
|
+
models_for_table(relation.name).any? { |m| m.sharded_column?(column) }
|
|
171
214
|
end
|
|
172
215
|
|
|
173
216
|
def sharded_primary_key?(relation, column)
|
|
174
217
|
column = column.to_s
|
|
175
|
-
return column ==
|
|
218
|
+
return column == "id" if relation.klass == ::ActiveRecord::Base
|
|
176
219
|
|
|
177
220
|
relation.klass.primary_key == column && relation.klass.integral_id?
|
|
178
221
|
end
|
|
179
222
|
|
|
180
223
|
def source_shard_for_foreign_key(relation, column)
|
|
181
224
|
reflection = nil
|
|
182
|
-
models_for_table(relation.
|
|
225
|
+
models_for_table(relation.name).each do |model|
|
|
183
226
|
reflection = model.send(:reflection_for_integer_attribute, column)
|
|
184
227
|
break if reflection
|
|
185
228
|
end
|
|
@@ -199,12 +242,11 @@ module Switchman
|
|
|
199
242
|
|
|
200
243
|
case opts
|
|
201
244
|
when String, Array
|
|
202
|
-
values = Hash === rest.first ? rest.first.values : rest
|
|
245
|
+
values = (Hash === rest.first) ? rest.first.values : rest
|
|
203
246
|
|
|
204
|
-
values.grep(ActiveRecord::Relation)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
rel.shard!(primary_shard) if rel.shard_source_value == :implicit && rel.primary_shard != primary_shard
|
|
247
|
+
if shard_source_value != :explicit && values.grep(ActiveRecord::Relation).first
|
|
248
|
+
raise "Sub-queries are not allowed as simple substitutions; " \
|
|
249
|
+
"please build your relation with more structured methods so that Switchman is able to introspect it."
|
|
208
250
|
end
|
|
209
251
|
|
|
210
252
|
super
|
|
@@ -221,31 +263,49 @@ module Switchman
|
|
|
221
263
|
end
|
|
222
264
|
end
|
|
223
265
|
|
|
224
|
-
def arel_columns(columns)
|
|
225
|
-
connection.with_local_table_name { super }
|
|
226
|
-
end
|
|
227
|
-
|
|
228
266
|
def arel_column(columns)
|
|
229
267
|
connection.with_local_table_name { super }
|
|
230
268
|
end
|
|
231
269
|
|
|
232
270
|
def table_name_matches?(from)
|
|
233
|
-
|
|
271
|
+
if ::Rails.version < "7.2"
|
|
272
|
+
connection.with_global_table_name { super }
|
|
273
|
+
else
|
|
274
|
+
connection.with_global_table_name do
|
|
275
|
+
table_name = Regexp.escape(table.name)
|
|
276
|
+
# INST: adapter_class -> connection
|
|
277
|
+
quoted_table_name = Regexp.escape(connection.quote_table_name(table.name))
|
|
278
|
+
/(?:\A|(?<!FROM)\s)(?:\b#{table_name}\b|#{quoted_table_name})(?!\.)/i.match?(from.to_s)
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
unless ::Rails.version < "7.2"
|
|
284
|
+
def order_column(field)
|
|
285
|
+
arel_column(field) do |attr_name|
|
|
286
|
+
if attr_name == "count" && !group_values.empty?
|
|
287
|
+
table[attr_name]
|
|
288
|
+
else
|
|
289
|
+
# INST: adapter_class -> connection
|
|
290
|
+
::Arel.sql(connection.quote_table_name(attr_name), retryable: true)
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
234
294
|
end
|
|
235
295
|
|
|
236
|
-
def each_predicate(predicates = nil, &
|
|
237
|
-
return predicates.map(&
|
|
296
|
+
def each_predicate(predicates = nil, &)
|
|
297
|
+
return predicates.map(&) if predicates
|
|
238
298
|
|
|
239
|
-
each_predicate_cb(:having_clause, :having_clause=, &
|
|
240
|
-
each_predicate_cb(:where_clause, :where_clause=, &
|
|
299
|
+
each_predicate_cb(:having_clause, :having_clause=, &)
|
|
300
|
+
each_predicate_cb(:where_clause, :where_clause=, &)
|
|
241
301
|
end
|
|
242
302
|
|
|
243
|
-
def each_predicate_cb(clause_getter, clause_setter, &
|
|
303
|
+
def each_predicate_cb(clause_getter, clause_setter, &)
|
|
244
304
|
old_clause = send(clause_getter)
|
|
245
305
|
old_predicates = old_clause.send(:predicates)
|
|
246
306
|
return if old_predicates.empty?
|
|
247
307
|
|
|
248
|
-
new_predicates = old_predicates.map(&
|
|
308
|
+
new_predicates = old_predicates.map(&)
|
|
249
309
|
return if new_predicates == old_predicates
|
|
250
310
|
|
|
251
311
|
new_clause = old_clause.dup
|
|
@@ -254,9 +314,10 @@ module Switchman
|
|
|
254
314
|
send(clause_setter, new_clause)
|
|
255
315
|
end
|
|
256
316
|
|
|
257
|
-
def each_transposable_predicate(predicates
|
|
317
|
+
def each_transposable_predicate(predicates, &block)
|
|
258
318
|
each_predicate(predicates) do |predicate|
|
|
259
|
-
|
|
319
|
+
case predicate
|
|
320
|
+
when ::Arel::Nodes::Grouping
|
|
260
321
|
next predicate unless predicate.expr.is_a?(::Arel::Nodes::Or)
|
|
261
322
|
|
|
262
323
|
or_expr = predicate.expr
|
|
@@ -266,7 +327,44 @@ module Switchman
|
|
|
266
327
|
|
|
267
328
|
next predicate if new_left == old_left && new_right == old_right
|
|
268
329
|
|
|
269
|
-
next predicate.class.new predicate.expr.class.new(new_left, new_right)
|
|
330
|
+
next predicate.class.new predicate.expr.class.new(new_left, new_right) if ::Rails.version < "7.2"
|
|
331
|
+
|
|
332
|
+
next predicate.class.new predicate.expr.class.new([new_left, new_right])
|
|
333
|
+
|
|
334
|
+
when ::Arel::Nodes::SelectStatement
|
|
335
|
+
new_cores = predicate.cores.map do |core|
|
|
336
|
+
next core unless core.is_a?(::Arel::Nodes::SelectCore) # just in case something weird is going on
|
|
337
|
+
|
|
338
|
+
new_wheres = each_transposable_predicate(core.wheres, &block)
|
|
339
|
+
new_havings = each_transposable_predicate(core.havings, &block)
|
|
340
|
+
|
|
341
|
+
next core if core.wheres == new_wheres && core.havings == new_havings
|
|
342
|
+
|
|
343
|
+
new_core = core.clone
|
|
344
|
+
new_core.wheres = new_wheres
|
|
345
|
+
new_core.havings = new_havings
|
|
346
|
+
new_core
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
next predicate if predicate.cores == new_cores
|
|
350
|
+
|
|
351
|
+
new_node = predicate.clone
|
|
352
|
+
new_node.instance_variable_set(:@cores, new_cores)
|
|
353
|
+
next new_node
|
|
354
|
+
when ::Arel::Nodes::Not
|
|
355
|
+
old_value = predicate.expr
|
|
356
|
+
new_value = each_transposable_predicate([old_value], &block).first
|
|
357
|
+
|
|
358
|
+
next predicate if old_value == new_value
|
|
359
|
+
|
|
360
|
+
next predicate.class.new(new_value)
|
|
361
|
+
when ::Arel::Nodes::Exists
|
|
362
|
+
old_value = predicate.expressions
|
|
363
|
+
new_value = each_transposable_predicate([old_value], &block).first
|
|
364
|
+
|
|
365
|
+
next predicate if old_value == new_value
|
|
366
|
+
|
|
367
|
+
next predicate.class.new(new_value)
|
|
270
368
|
end
|
|
271
369
|
|
|
272
370
|
next predicate unless predicate.is_a?(::Arel::Nodes::Binary) || predicate.is_a?(::Arel::Nodes::HomogeneousIn)
|
|
@@ -279,54 +377,56 @@ module Switchman
|
|
|
279
377
|
end
|
|
280
378
|
end
|
|
281
379
|
|
|
282
|
-
def each_transposable_predicate_value(predicates = nil)
|
|
380
|
+
def each_transposable_predicate_value(predicates = nil, &block)
|
|
283
381
|
each_transposable_predicate(predicates) do |predicate, relation, column, type|
|
|
284
|
-
each_transposable_predicate_value_cb(predicate) do |value|
|
|
382
|
+
each_transposable_predicate_value_cb(predicate, block) do |value|
|
|
285
383
|
yield(value, predicate, relation, column, type)
|
|
286
384
|
end
|
|
287
385
|
end
|
|
288
386
|
end
|
|
289
387
|
|
|
290
|
-
def each_transposable_predicate_value_cb(node, &
|
|
388
|
+
def each_transposable_predicate_value_cb(node, original_block, &)
|
|
291
389
|
case node
|
|
292
390
|
when Array
|
|
293
|
-
node.
|
|
391
|
+
node.filter_map { |val| each_transposable_predicate_value_cb(val, original_block, &).presence }
|
|
294
392
|
when ::ActiveModel::Attribute
|
|
295
393
|
old_value = node.value_before_type_cast
|
|
296
|
-
new_value = each_transposable_predicate_value_cb(old_value, &
|
|
394
|
+
new_value = each_transposable_predicate_value_cb(old_value, original_block, &)
|
|
297
395
|
|
|
298
|
-
old_value == new_value ? node : node.class.new(node.name, new_value, node.type)
|
|
396
|
+
(old_value == new_value) ? node : node.class.new(node.name, new_value, node.type)
|
|
299
397
|
when ::Arel::Nodes::And
|
|
300
398
|
old_value = node.children
|
|
301
|
-
new_value = each_transposable_predicate_value_cb(old_value, &
|
|
399
|
+
new_value = each_transposable_predicate_value_cb(old_value, original_block, &)
|
|
302
400
|
|
|
303
|
-
old_value == new_value ? node : node.class.new(new_value)
|
|
401
|
+
(old_value == new_value) ? node : node.class.new(new_value)
|
|
304
402
|
when ::Arel::Nodes::BindParam
|
|
305
403
|
old_value = node.value
|
|
306
|
-
new_value = each_transposable_predicate_value_cb(old_value, &
|
|
404
|
+
new_value = each_transposable_predicate_value_cb(old_value, original_block, &)
|
|
307
405
|
|
|
308
|
-
old_value == new_value ? node : node.class.new(new_value)
|
|
406
|
+
(old_value == new_value) ? node : node.class.new(new_value)
|
|
309
407
|
when ::Arel::Nodes::Casted
|
|
310
408
|
old_value = node.value
|
|
311
|
-
new_value = each_transposable_predicate_value_cb(old_value, &
|
|
409
|
+
new_value = each_transposable_predicate_value_cb(old_value, original_block, &)
|
|
312
410
|
|
|
313
|
-
old_value == new_value ? node : node.class.new(new_value, node.attribute)
|
|
411
|
+
(old_value == new_value) ? node : node.class.new(new_value, node.attribute)
|
|
314
412
|
when ::Arel::Nodes::HomogeneousIn
|
|
315
413
|
old_value = node.values
|
|
316
|
-
new_value = each_transposable_predicate_value_cb(old_value, &
|
|
414
|
+
new_value = each_transposable_predicate_value_cb(old_value, original_block, &)
|
|
317
415
|
|
|
318
416
|
# switch to a regular In, so that Relation::WhereClause#contradiction? knows about it
|
|
319
417
|
if new_value.empty?
|
|
320
|
-
klass = node.type == :in ? ::Arel::Nodes::In : ::Arel::Nodes::NotIn
|
|
418
|
+
klass = (node.type == :in) ? ::Arel::Nodes::In : ::Arel::Nodes::NotIn
|
|
321
419
|
klass.new(node.attribute, new_value)
|
|
322
420
|
else
|
|
323
|
-
old_value == new_value ? node : node.class.new(new_value, node.attribute, node.type)
|
|
421
|
+
(old_value == new_value) ? node : node.class.new(new_value, node.attribute, node.type)
|
|
324
422
|
end
|
|
325
423
|
when ::Arel::Nodes::Binary
|
|
326
424
|
old_value = node.right
|
|
327
|
-
new_value = each_transposable_predicate_value_cb(old_value, &
|
|
425
|
+
new_value = each_transposable_predicate_value_cb(old_value, original_block, &)
|
|
328
426
|
|
|
329
|
-
old_value == new_value ? node : node.class.new(node.left, new_value)
|
|
427
|
+
(old_value == new_value) ? node : node.class.new(node.left, new_value)
|
|
428
|
+
when ::Arel::Nodes::SelectStatement
|
|
429
|
+
each_transposable_predicate_value([node], &original_block).first
|
|
330
430
|
else
|
|
331
431
|
yield(node)
|
|
332
432
|
end
|
|
@@ -343,6 +443,8 @@ module Switchman
|
|
|
343
443
|
Shard.current(klass.connection_class_for_self)
|
|
344
444
|
elsif type == :foreign
|
|
345
445
|
source_shard_for_foreign_key(relation, column)
|
|
446
|
+
else
|
|
447
|
+
primary_shard
|
|
346
448
|
end
|
|
347
449
|
|
|
348
450
|
transpose_predicate_value(value, current_source_shard, target_shard, type)
|
|
@@ -350,7 +452,9 @@ module Switchman
|
|
|
350
452
|
end
|
|
351
453
|
|
|
352
454
|
def transpose_predicate_value(value, current_shard, target_shard, attribute_type)
|
|
353
|
-
if value.is_a?(
|
|
455
|
+
if value.is_a?(NonTransposingValue)
|
|
456
|
+
value
|
|
457
|
+
elsif value.is_a?(::ActiveRecord::StatementCache::Substitute)
|
|
354
458
|
value.sharded = true # mark for transposition later
|
|
355
459
|
value.primary = true if attribute_type == :primary
|
|
356
460
|
value
|
|
@@ -28,11 +28,18 @@ module Switchman
|
|
|
28
28
|
# this technically belongs on AssociationReflection, but we put it on
|
|
29
29
|
# ThroughReflection as well, instead of delegating to its internal
|
|
30
30
|
# HasManyAssociation, losing its proper `klass`
|
|
31
|
-
def association_scope_cache(klass, owner, &
|
|
31
|
+
def association_scope_cache(klass, owner, &)
|
|
32
32
|
key = self
|
|
33
33
|
key = [key, owner._read_attribute(@foreign_type)] if polymorphic?
|
|
34
34
|
key = [key, shard(owner).id].flatten
|
|
35
|
-
|
|
35
|
+
|
|
36
|
+
if ::Rails.version < "7.2"
|
|
37
|
+
klass.cached_find_by_statement(key, &)
|
|
38
|
+
else
|
|
39
|
+
klass.with_connection do |connection|
|
|
40
|
+
klass.cached_find_by_statement(connection, key, &)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
36
43
|
end
|
|
37
44
|
end
|
|
38
45
|
|
|
@@ -28,15 +28,15 @@ module Switchman
|
|
|
28
28
|
relation
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
def new(*, &
|
|
31
|
+
def new(*, &)
|
|
32
32
|
primary_shard.activate(klass.connection_class_for_self) { super }
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
def create(*, &
|
|
35
|
+
def create(*, &)
|
|
36
36
|
primary_shard.activate(klass.connection_class_for_self) { super }
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
def create!(*, &
|
|
39
|
+
def create!(*, &)
|
|
40
40
|
primary_shard.activate(klass.connection_class_for_self) { super }
|
|
41
41
|
end
|
|
42
42
|
|
|
@@ -44,12 +44,18 @@ module Switchman
|
|
|
44
44
|
primary_shard.activate(klass.connection_class_for_self) { super }
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
if ::Rails.version > "7.1.2"
|
|
48
|
+
def transaction(...)
|
|
49
|
+
primary_shard.activate(klass.connection_class_for_self) { super }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def explain(*args)
|
|
54
|
+
activate { |relation| relation.call_super(:explain, Relation, *args) }
|
|
49
55
|
end
|
|
50
56
|
|
|
51
57
|
def load(&block)
|
|
52
|
-
if !loaded? ||
|
|
58
|
+
if !loaded? || scheduled?
|
|
53
59
|
@records = activate { |relation| relation.send(:exec_queries, &block) }
|
|
54
60
|
@loaded = true
|
|
55
61
|
end
|
|
@@ -58,27 +64,83 @@ module Switchman
|
|
|
58
64
|
end
|
|
59
65
|
|
|
60
66
|
%I[update_all delete_all].each do |method|
|
|
61
|
-
arg_params = RUBY_VERSION <= '2.8' ? '*args' : '*args, **kwargs'
|
|
62
67
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
63
|
-
def #{method}(
|
|
64
|
-
result = self.activate(unordered: true) { |relation| relation.call_super(#{method.inspect}, Relation,
|
|
68
|
+
def #{method}(*args, **kwargs)
|
|
69
|
+
result = self.activate(unordered: true) { |relation| relation.call_super(#{method.inspect}, Relation, *args, **kwargs) }
|
|
65
70
|
result = result.sum if result.is_a?(Array)
|
|
66
71
|
result
|
|
67
72
|
end
|
|
68
73
|
RUBY
|
|
69
74
|
end
|
|
70
75
|
|
|
76
|
+
# https://github.com/rails/rails/commit/ed2c15b52450ff927a05629f031376f25b670335
|
|
77
|
+
# once the minimum version is Rails 7.2, we can drop this separate module
|
|
78
|
+
module InsertUpsertAll
|
|
79
|
+
%w[insert_all upsert_all].each do |method|
|
|
80
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
81
|
+
def #{method}(attributes, returning: nil, **)
|
|
82
|
+
scope = self != ::ActiveRecord::Base && current_scope
|
|
83
|
+
if (target_shard = scope&.primary_shard) == (current_shard = Shard.current(connection_class_for_self))
|
|
84
|
+
scope = nil
|
|
85
|
+
end
|
|
86
|
+
if scope
|
|
87
|
+
dupped = false
|
|
88
|
+
attributes.each_with_index do |hash, i|
|
|
89
|
+
if dupped || hash.any? { |k, v| sharded_column?(k) }
|
|
90
|
+
unless dupped
|
|
91
|
+
attributes = attributes.dup
|
|
92
|
+
dupped = true
|
|
93
|
+
end
|
|
94
|
+
attributes[i] = hash.to_h do |k, v|
|
|
95
|
+
if sharded_column?(k)
|
|
96
|
+
[k, Shard.relative_id_for(v, current_shard, target_shard)]
|
|
97
|
+
else
|
|
98
|
+
[k, v]
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
if scope
|
|
106
|
+
scope.activate do
|
|
107
|
+
db = Shard.current(connection_class_for_self).database_server
|
|
108
|
+
result = db.unguard { super }
|
|
109
|
+
if result&.columns&.any? { |c| sharded_column?(c) }
|
|
110
|
+
transposed_rows = result.rows.map do |row|
|
|
111
|
+
row.map.with_index do |value, i|
|
|
112
|
+
sharded_column?(result.columns[i]) ? Shard.relative_id_for(value, target_shard, current_shard) : value
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
result = ::ActiveRecord::Result.new(result.columns, transposed_rows, result.column_types)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
result
|
|
119
|
+
end
|
|
120
|
+
else
|
|
121
|
+
db = Shard.current(connection_class_for_self).database_server
|
|
122
|
+
db.unguard { super }
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
RUBY
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
71
129
|
def find_ids_in_ranges(options = {})
|
|
72
130
|
is_integer = columns_hash[primary_key.to_s].type == :integer
|
|
73
131
|
loose_mode = options[:loose] && is_integer
|
|
74
132
|
# loose_mode: if we don't care about getting exactly batch_size ids in between
|
|
75
133
|
# don't get the max - just get the min and add batch_size so we get that many _at most_
|
|
76
|
-
values = loose_mode ?
|
|
134
|
+
values = loose_mode ? "MIN(id)" : "MIN(id), MAX(id)"
|
|
77
135
|
|
|
78
136
|
batch_size = options[:batch_size].try(:to_i) || 1000
|
|
79
|
-
quoted_primary_key =
|
|
80
|
-
|
|
81
|
-
|
|
137
|
+
quoted_primary_key =
|
|
138
|
+
"#{klass.connection.quote_local_table_name(table_name)}.#{klass.connection.quote_column_name(primary_key)}"
|
|
139
|
+
as_id = " AS id" unless primary_key == "id"
|
|
140
|
+
subquery_scope = except(:select)
|
|
141
|
+
.select("#{quoted_primary_key}#{as_id}")
|
|
142
|
+
.reorder(primary_key.to_sym)
|
|
143
|
+
.limit(loose_mode ? 1 : batch_size)
|
|
82
144
|
subquery_scope = subquery_scope.where("#{quoted_primary_key} <= ?", options[:end_at]) if options[:end_at]
|
|
83
145
|
|
|
84
146
|
first_subquery_scope = if options[:start_at]
|
|
@@ -101,7 +163,7 @@ module Switchman
|
|
|
101
163
|
end
|
|
102
164
|
end
|
|
103
165
|
|
|
104
|
-
def activate(unordered: false, &block)
|
|
166
|
+
def activate(count: false, unordered: false, &block)
|
|
105
167
|
shards = all_shards
|
|
106
168
|
if Array === shards && shards.length == 1
|
|
107
169
|
if !loaded? && shard_value != shards.first
|
|
@@ -121,11 +183,19 @@ module Switchman
|
|
|
121
183
|
relation = shard(Shard.current(klass.connection_class_for_self))
|
|
122
184
|
relation.remove_nonlocal_primary_keys!
|
|
123
185
|
# do a minimal query if possible
|
|
124
|
-
|
|
186
|
+
if limit_value && !result_count.zero? && order_values.empty?
|
|
187
|
+
relation = relation.limit(limit_value - result_count)
|
|
188
|
+
end
|
|
125
189
|
|
|
126
190
|
shard_results = relation.activate(&block)
|
|
127
191
|
|
|
128
|
-
if shard_results.present? &&
|
|
192
|
+
if shard_results.present? && count
|
|
193
|
+
unless shard_results.is_a?(Integer)
|
|
194
|
+
raise "expected integer result for count, got #{shard_results.class.name}"
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
result_count += shard_results
|
|
198
|
+
elsif shard_results.present? && !unordered
|
|
129
199
|
can_order ||= can_order_cross_shard_results? unless order_values.empty?
|
|
130
200
|
raise OrderOnMultiShardQuery if !can_order && !order_values.empty? && result_count.positive?
|
|
131
201
|
|
|
@@ -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
|
|
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
|
|
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]
|
|
@@ -62,16 +62,12 @@ module Switchman
|
|
|
62
62
|
if primary_shard != final_primary_shard && rhs.primary_shard != final_primary_shard
|
|
63
63
|
shard!(final_primary_shard)
|
|
64
64
|
rhs = rhs.shard(final_primary_shard)
|
|
65
|
-
super(rhs)
|
|
66
65
|
elsif primary_shard != final_primary_shard
|
|
67
66
|
shard!(final_primary_shard)
|
|
68
|
-
super(rhs)
|
|
69
67
|
elsif rhs.primary_shard != final_primary_shard
|
|
70
68
|
rhs = rhs.shard(final_primary_shard)
|
|
71
|
-
super(rhs)
|
|
72
|
-
else
|
|
73
|
-
super
|
|
74
69
|
end
|
|
70
|
+
super
|
|
75
71
|
|
|
76
72
|
self.shard_value = final_shard_value
|
|
77
73
|
self.shard_source_value = 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
|
|
8
|
-
relation =
|
|
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)
|
|
@@ -29,14 +29,14 @@ module Switchman
|
|
|
29
29
|
params, connection = args
|
|
30
30
|
klass = @klass
|
|
31
31
|
target_shard = nil
|
|
32
|
-
if (primary_index = bind_map.primary_value_index)
|
|
32
|
+
if (primary_index = @bind_map.primary_value_index)
|
|
33
33
|
primary_value = params[primary_index]
|
|
34
34
|
target_shard = Shard.local_id_for(primary_value)[1]
|
|
35
35
|
end
|
|
36
36
|
current_shard = Shard.current(klass.connection_class_for_self)
|
|
37
37
|
target_shard ||= current_shard
|
|
38
38
|
|
|
39
|
-
bind_values = bind_map.bind(params, current_shard, target_shard)
|
|
39
|
+
bind_values = @bind_map.bind(params, current_shard, target_shard)
|
|
40
40
|
|
|
41
41
|
target_shard.activate(klass.connection_class_for_self) do
|
|
42
42
|
sql = qualified_query_builder(target_shard, klass).sql_for(bind_values, connection)
|
|
@@ -7,9 +7,14 @@ module Switchman
|
|
|
7
7
|
def drop(*)
|
|
8
8
|
super
|
|
9
9
|
# no really, it's gone
|
|
10
|
-
Switchman.cache.delete(
|
|
10
|
+
Switchman.cache.delete("default_shard")
|
|
11
11
|
Shard.default(reload: true)
|
|
12
12
|
end
|
|
13
|
+
|
|
14
|
+
def raise_for_multi_db(*)
|
|
15
|
+
# ignore; Switchman doesn't use namespaced tasks for multiple shards; it uses
|
|
16
|
+
# environment variables to filter which shards you want to target
|
|
17
|
+
end
|
|
13
18
|
end
|
|
14
19
|
end
|
|
15
20
|
end
|