switchman 1.5.13 → 2.0.9
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 +5 -5
- data/app/models/switchman/shard.rb +746 -11
- data/db/migrate/20130328212039_create_switchman_shards.rb +3 -1
- data/db/migrate/20130328224244_create_default_shard.rb +4 -2
- data/db/migrate/20161206323434_add_back_default_string_limits_switchman.rb +13 -0
- data/db/migrate/20180828183945_add_default_shard_index.rb +15 -0
- data/db/migrate/20180828192111_add_timestamps_to_shards.rb +17 -0
- data/db/migrate/20190114212900_add_unique_name_indexes.rb +9 -0
- data/lib/switchman/action_controller/caching.rb +2 -0
- data/lib/switchman/active_record/abstract_adapter.rb +14 -4
- data/lib/switchman/active_record/association.rb +64 -37
- data/lib/switchman/active_record/attribute_methods.rb +16 -5
- data/lib/switchman/active_record/base.rb +67 -22
- data/lib/switchman/active_record/batches.rb +3 -1
- data/lib/switchman/active_record/calculations.rb +16 -21
- data/lib/switchman/active_record/connection_handler.rb +71 -79
- data/lib/switchman/active_record/connection_pool.rb +28 -23
- data/lib/switchman/active_record/finder_methods.rb +37 -28
- data/lib/switchman/active_record/log_subscriber.rb +14 -19
- data/lib/switchman/active_record/migration.rb +80 -0
- data/lib/switchman/active_record/model_schema.rb +3 -1
- data/lib/switchman/active_record/persistence.rb +9 -1
- data/lib/switchman/active_record/postgresql_adapter.rb +166 -126
- data/lib/switchman/active_record/predicate_builder.rb +2 -0
- data/lib/switchman/active_record/query_cache.rb +22 -87
- data/lib/switchman/active_record/query_methods.rb +123 -124
- data/lib/switchman/active_record/reflection.rb +37 -20
- data/lib/switchman/active_record/relation.rb +50 -29
- data/lib/switchman/active_record/spawn_methods.rb +2 -0
- data/lib/switchman/active_record/statement_cache.rb +44 -52
- data/lib/switchman/active_record/table_definition.rb +4 -2
- data/lib/switchman/active_record/type_caster.rb +2 -0
- data/lib/switchman/active_record/where_clause_factory.rb +5 -2
- data/lib/switchman/active_support/cache.rb +18 -0
- data/lib/switchman/arel.rb +8 -25
- data/lib/switchman/call_super.rb +19 -0
- data/lib/switchman/connection_pool_proxy.rb +70 -24
- data/lib/switchman/database_server.rb +72 -60
- data/lib/switchman/default_shard.rb +3 -0
- data/lib/switchman/engine.rb +45 -40
- data/lib/switchman/environment.rb +2 -0
- data/lib/switchman/errors.rb +3 -0
- data/lib/switchman/{shackles → guard_rail}/relation.rb +7 -5
- data/lib/switchman/{shackles.rb → guard_rail.rb} +6 -4
- data/lib/switchman/open4.rb +80 -0
- data/lib/switchman/r_spec_helper.rb +14 -8
- data/lib/switchman/rails.rb +2 -0
- data/lib/switchman/schema_cache.rb +17 -0
- data/lib/switchman/sharded_instrumenter.rb +4 -2
- data/lib/switchman/standard_error.rb +14 -0
- data/lib/switchman/test_helper.rb +6 -3
- data/lib/switchman/version.rb +3 -1
- data/lib/switchman.rb +4 -2
- data/lib/tasks/switchman.rake +54 -68
- metadata +87 -40
- data/app/models/switchman/shard_internal.rb +0 -659
- data/lib/switchman/engine.rb~ +0 -203
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Switchman
|
|
2
4
|
module ActiveRecord
|
|
3
5
|
module QueryMethods
|
|
@@ -19,11 +21,11 @@ module Switchman
|
|
|
19
21
|
@values[:shard_source]
|
|
20
22
|
end
|
|
21
23
|
def shard_value=(value)
|
|
22
|
-
raise ImmutableRelation if @loaded
|
|
24
|
+
raise ::ActiveRecord::ImmutableRelation if @loaded
|
|
23
25
|
@values[:shard] = value
|
|
24
26
|
end
|
|
25
27
|
def shard_source_value=(value)
|
|
26
|
-
raise ImmutableRelation if @loaded
|
|
28
|
+
raise ::ActiveRecord::ImmutableRelation if @loaded
|
|
27
29
|
@values[:shard_source] = value
|
|
28
30
|
end
|
|
29
31
|
|
|
@@ -42,34 +44,6 @@ module Switchman
|
|
|
42
44
|
self
|
|
43
45
|
end
|
|
44
46
|
|
|
45
|
-
if ::Rails.version < '5'
|
|
46
|
-
# moved to WhereClauseFactory#build in Rails 5
|
|
47
|
-
def build_where(opts, other = [])
|
|
48
|
-
case opts
|
|
49
|
-
when String, Array
|
|
50
|
-
values = Hash === other.first ? other.first.values : other
|
|
51
|
-
|
|
52
|
-
values.grep(ActiveRecord::Relation) do |rel|
|
|
53
|
-
# serialize subqueries against the same shard as the outer query is currently
|
|
54
|
-
# targeted to run against
|
|
55
|
-
if rel.shard_source_value == :implicit && rel.primary_shard != primary_shard
|
|
56
|
-
rel.shard!(primary_shard)
|
|
57
|
-
end
|
|
58
|
-
self.bind_values += rel.bind_values if ::Rails.version < '4.2'
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
[@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
|
|
62
|
-
when Hash, ::Arel::Nodes::Node
|
|
63
|
-
predicates = super
|
|
64
|
-
infer_shards_from_primary_key(predicates) if shard_source_value == :implicit && shard_value.is_a?(Shard)
|
|
65
|
-
predicates = transpose_predicates(predicates, nil, primary_shard) if shard_source_value != :explicit
|
|
66
|
-
predicates
|
|
67
|
-
else
|
|
68
|
-
super
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
|
|
73
47
|
# the shard that where_values are relative to. if it's multiple shards, they're stored
|
|
74
48
|
# relative to the first shard
|
|
75
49
|
def primary_shard
|
|
@@ -104,44 +78,47 @@ module Switchman
|
|
|
104
78
|
end
|
|
105
79
|
end
|
|
106
80
|
|
|
107
|
-
if ::Rails.version >= '4.2' && ::Rails.version < '5'
|
|
108
|
-
# fixes an issue in Rails 4.2 with `reverse_sql_order` and qualified names
|
|
109
|
-
# where quoted_table_name is called before shard(s) have been activated
|
|
110
|
-
# if there's no ordering
|
|
111
|
-
def reverse_order!
|
|
112
|
-
orders = order_values.uniq
|
|
113
|
-
orders.reject!(&:blank?)
|
|
114
|
-
if orders.empty?
|
|
115
|
-
self.order_values = [arel_table[primary_key].desc]
|
|
116
|
-
else
|
|
117
|
-
self.order_values = reverse_sql_order(orders)
|
|
118
|
-
end
|
|
119
|
-
self
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
|
|
123
81
|
private
|
|
124
82
|
|
|
125
|
-
|
|
126
|
-
|
|
83
|
+
if ::Rails.version >= '5.2'
|
|
84
|
+
[:where, :having].each do |type|
|
|
85
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
127
86
|
def transpose_#{type}_clauses(source_shard, target_shard, remove_nonlocal_primary_keys)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
87
|
+
unless (predicates = #{type}_clause.send(:predicates)).empty?
|
|
88
|
+
new_predicates, _binds = transpose_predicates(predicates, source_shard,
|
|
89
|
+
target_shard, remove_nonlocal_primary_keys)
|
|
90
|
+
if new_predicates != predicates
|
|
91
|
+
self.#{type}_clause = #{type}_clause.dup
|
|
132
92
|
if new_predicates != predicates
|
|
133
|
-
self.#{type}_clause = #{type}_clause.dup
|
|
134
93
|
#{type}_clause.instance_variable_set(:@predicates, new_predicates)
|
|
135
94
|
end
|
|
136
95
|
end
|
|
137
|
-
else
|
|
138
|
-
unless #{type}_values.empty?
|
|
139
|
-
self.#{type}_values = transpose_predicates(#{type}_values,
|
|
140
|
-
source_shard, target_shard, remove_nonlocal_primary_keys)
|
|
141
|
-
end
|
|
142
96
|
end
|
|
143
97
|
end
|
|
144
|
-
|
|
98
|
+
RUBY
|
|
99
|
+
end
|
|
100
|
+
else
|
|
101
|
+
[:where, :having].each do |type|
|
|
102
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
103
|
+
def transpose_#{type}_clauses(source_shard, target_shard, remove_nonlocal_primary_keys)
|
|
104
|
+
unless (predicates = #{type}_clause.send(:predicates)).empty?
|
|
105
|
+
new_predicates, new_binds = transpose_predicates(predicates, source_shard,
|
|
106
|
+
target_shard, remove_nonlocal_primary_keys,
|
|
107
|
+
binds: #{type}_clause.binds,
|
|
108
|
+
dup_binds_on_mutation: true)
|
|
109
|
+
if new_predicates != predicates || !new_binds.equal?(#{type}_clause.binds)
|
|
110
|
+
self.#{type}_clause = #{type}_clause.dup
|
|
111
|
+
if new_predicates != predicates
|
|
112
|
+
#{type}_clause.instance_variable_set(:@predicates, new_predicates)
|
|
113
|
+
end
|
|
114
|
+
if !new_binds.equal?(#{type}_clause.binds)
|
|
115
|
+
#{type}_clause.instance_variable_set(:@binds, new_binds)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
RUBY
|
|
121
|
+
end
|
|
145
122
|
end
|
|
146
123
|
|
|
147
124
|
def transpose_clauses(source_shard, target_shard, remove_nonlocal_primary_keys = false)
|
|
@@ -150,6 +127,8 @@ module Switchman
|
|
|
150
127
|
end
|
|
151
128
|
|
|
152
129
|
def infer_shards_from_primary_key(predicates, binds = nil)
|
|
130
|
+
return unless klass.integral_id?
|
|
131
|
+
|
|
153
132
|
primary_key = predicates.detect do |predicate|
|
|
154
133
|
predicate.is_a?(::Arel::Nodes::Binary) && predicate.left.is_a?(::Arel::Attributes::Attribute) &&
|
|
155
134
|
predicate.left.relation.is_a?(::Arel::Table) && predicate.left.relation.model == klass &&
|
|
@@ -180,28 +159,23 @@ module Switchman
|
|
|
180
159
|
return
|
|
181
160
|
end
|
|
182
161
|
when ::Arel::Nodes::BindParam
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
162
|
+
if ::Rails.version >= "5.2"
|
|
163
|
+
local_id, id_shard = Shard.local_id_for(primary_key.right.value.value_before_type_cast)
|
|
164
|
+
id_shard ||= Shard.current(klass.shard_category) if local_id
|
|
165
|
+
else
|
|
166
|
+
# look for a bind param with a matching column name
|
|
167
|
+
if binds && bind = binds.detect{|b| b&.name.to_s == klass.primary_key.to_s}
|
|
187
168
|
unless bind.value.is_a?(::ActiveRecord::StatementCache::Substitute)
|
|
188
169
|
local_id, id_shard = Shard.local_id_for(bind.value)
|
|
189
170
|
id_shard ||= Shard.current(klass.shard_category) if local_id
|
|
190
171
|
end
|
|
191
172
|
end
|
|
192
|
-
else
|
|
193
|
-
if bind_values && idx = bind_values.find_index{|b| b.is_a?(Array) && b.first.try(:name).to_s == klass.primary_key.to_s}
|
|
194
|
-
column, value = bind_values[idx]
|
|
195
|
-
unless ::Rails.version >= '4.2' && value.is_a?(::ActiveRecord::StatementCache::Substitute)
|
|
196
|
-
local_id, id_shard = Shard.local_id_for(value)
|
|
197
|
-
id_shard ||= Shard.current(klass.shard_category) if local_id
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
173
|
end
|
|
201
174
|
else
|
|
202
175
|
local_id, id_shard = Shard.local_id_for(primary_key.right)
|
|
203
176
|
id_shard ||= Shard.current(klass.shard_category) if local_id
|
|
204
177
|
end
|
|
178
|
+
|
|
205
179
|
return if !id_shard || id_shard == primary_shard
|
|
206
180
|
transpose_clauses(primary_shard, id_shard)
|
|
207
181
|
self.shard_value = id_shard
|
|
@@ -228,7 +202,7 @@ module Switchman
|
|
|
228
202
|
def sharded_primary_key?(relation, column)
|
|
229
203
|
column = column.to_s
|
|
230
204
|
return column == 'id' if relation.model == ::ActiveRecord::Base
|
|
231
|
-
relation.model.primary_key == column
|
|
205
|
+
relation.model.primary_key == column && relation.model.integral_id?
|
|
232
206
|
end
|
|
233
207
|
|
|
234
208
|
def source_shard_for_foreign_key(relation, column)
|
|
@@ -252,25 +226,22 @@ module Switchman
|
|
|
252
226
|
end
|
|
253
227
|
|
|
254
228
|
def arel_columns(columns)
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
elsif Symbol === field
|
|
261
|
-
# the rest of this is pulled from AR - the only change is from quote_table_name to quote_column_name here
|
|
262
|
-
# otherwise qualified names will add the schema to a column
|
|
263
|
-
connection.quote_column_name(field.to_s)
|
|
264
|
-
else
|
|
265
|
-
field
|
|
266
|
-
end
|
|
267
|
-
end
|
|
229
|
+
connection.with_local_table_name { super }
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def arel_column(columns)
|
|
233
|
+
connection.with_local_table_name { super }
|
|
268
234
|
end
|
|
269
235
|
|
|
270
236
|
# semi-private
|
|
271
237
|
public
|
|
272
|
-
def transpose_predicates(predicates,
|
|
273
|
-
|
|
238
|
+
def transpose_predicates(predicates,
|
|
239
|
+
source_shard,
|
|
240
|
+
target_shard,
|
|
241
|
+
remove_nonlocal_primary_keys = false,
|
|
242
|
+
binds: nil,
|
|
243
|
+
dup_binds_on_mutation: false)
|
|
244
|
+
result = predicates.map do |predicate|
|
|
274
245
|
next predicate unless predicate.is_a?(::Arel::Nodes::Binary)
|
|
275
246
|
next predicate unless predicate.left.is_a?(::Arel::Attributes::Attribute)
|
|
276
247
|
relation, column = relation_and_column(predicate.left)
|
|
@@ -290,28 +261,42 @@ module Switchman
|
|
|
290
261
|
source_shard_for_foreign_key(relation, column)
|
|
291
262
|
end
|
|
292
263
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
264
|
+
if ::Rails.version >= "5.2"
|
|
265
|
+
new_right_value =
|
|
266
|
+
case predicate.right
|
|
267
|
+
when Array
|
|
268
|
+
predicate.right.map {|val| transpose_predicate_value(val, current_source_shard, target_shard, type, remove) }
|
|
269
|
+
else
|
|
270
|
+
transpose_predicate_value(predicate.right, current_source_shard, target_shard, type, remove)
|
|
271
|
+
end
|
|
272
|
+
else
|
|
273
|
+
new_right_value = case predicate.right
|
|
274
|
+
when Array
|
|
275
|
+
local_ids = []
|
|
276
|
+
predicate.right.each do |value|
|
|
277
|
+
local_id = Shard.relative_id_for(value, current_source_shard, target_shard)
|
|
278
|
+
next unless local_id
|
|
279
|
+
unless remove && local_id > Shard::IDS_PER_SHARD
|
|
280
|
+
if value.is_a?(::Arel::Nodes::Casted)
|
|
281
|
+
if local_id == value.val
|
|
282
|
+
local_id = value
|
|
283
|
+
elsif local_id != value
|
|
284
|
+
local_id = value.class.new(local_id, value.attribute)
|
|
285
|
+
end
|
|
304
286
|
end
|
|
287
|
+
local_ids << local_id
|
|
305
288
|
end
|
|
306
|
-
local_ids << local_id
|
|
307
289
|
end
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
290
|
+
local_ids
|
|
291
|
+
when ::Arel::Nodes::BindParam
|
|
292
|
+
# look for a bind param with a matching column name
|
|
293
|
+
if binds && bind = binds.detect{|b| b&.name.to_s == predicate.left.name.to_s}
|
|
294
|
+
# before we mutate, dup
|
|
295
|
+
if dup_binds_on_mutation
|
|
296
|
+
binds = binds.map(&:dup)
|
|
297
|
+
dup_binds_on_mutation = false
|
|
298
|
+
bind = binds.find { |b| b&.name.to_s == predicate.left.name.to_s }
|
|
299
|
+
end
|
|
315
300
|
if bind.value.is_a?(::ActiveRecord::StatementCache::Substitute)
|
|
316
301
|
bind.value.sharded = true # mark for transposition later
|
|
317
302
|
bind.value.primary = true if type == :primary
|
|
@@ -322,29 +307,16 @@ module Switchman
|
|
|
322
307
|
bind.instance_variable_set(:@value_for_database, nil)
|
|
323
308
|
end
|
|
324
309
|
end
|
|
310
|
+
predicate.right
|
|
325
311
|
else
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
value.sharded = true # mark for transposition later
|
|
330
|
-
value.primary = true if type == :primary
|
|
331
|
-
else
|
|
332
|
-
local_id = Shard.relative_id_for(value, current_source_shard, target_shard)
|
|
333
|
-
local_id = [] if remove && local_id > Shard::IDS_PER_SHARD
|
|
334
|
-
bind_values[idx] = [column, local_id]
|
|
335
|
-
end
|
|
336
|
-
end
|
|
312
|
+
local_id = Shard.relative_id_for(predicate.right, current_source_shard, target_shard) || predicate.right
|
|
313
|
+
local_id = [] if remove && local_id.is_a?(Integer) && local_id > Shard::IDS_PER_SHARD
|
|
314
|
+
local_id
|
|
337
315
|
end
|
|
338
|
-
predicate.right
|
|
339
|
-
else
|
|
340
|
-
local_id = Shard.relative_id_for(predicate.right, current_source_shard, target_shard)
|
|
341
|
-
local_id = [] if remove && local_id.is_a?(Fixnum) && local_id > Shard::IDS_PER_SHARD
|
|
342
|
-
local_id
|
|
343
316
|
end
|
|
344
|
-
|
|
345
317
|
if new_right_value == predicate.right
|
|
346
318
|
predicate
|
|
347
|
-
elsif
|
|
319
|
+
elsif predicate.right.is_a?(::Arel::Nodes::Casted)
|
|
348
320
|
if new_right_value == predicate.right.val
|
|
349
321
|
predicate
|
|
350
322
|
else
|
|
@@ -354,6 +326,33 @@ module Switchman
|
|
|
354
326
|
predicate.class.new(predicate.left, new_right_value)
|
|
355
327
|
end
|
|
356
328
|
end
|
|
329
|
+
result = [result, binds]
|
|
330
|
+
result
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def transpose_predicate_value(value, current_shard, target_shard, attribute_type, remove_non_local_ids)
|
|
334
|
+
if value.is_a?(::Arel::Nodes::BindParam)
|
|
335
|
+
query_att = value.value
|
|
336
|
+
current_id = query_att.value_before_type_cast
|
|
337
|
+
if current_id.is_a?(::ActiveRecord::StatementCache::Substitute)
|
|
338
|
+
current_id.sharded = true # mark for transposition later
|
|
339
|
+
current_id.primary = true if attribute_type == :primary
|
|
340
|
+
value
|
|
341
|
+
else
|
|
342
|
+
local_id = Shard.relative_id_for(current_id, current_shard, target_shard) || current_id
|
|
343
|
+
local_id = [] if remove_non_local_ids && local_id.is_a?(Integer) && local_id > Shard::IDS_PER_SHARD
|
|
344
|
+
if current_id != local_id
|
|
345
|
+
# make a new bind param
|
|
346
|
+
::Arel::Nodes::BindParam.new(query_att.class.new(query_att.name, local_id, query_att.type))
|
|
347
|
+
else
|
|
348
|
+
value
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
else
|
|
352
|
+
local_id = Shard.relative_id_for(value, current_shard, target_shard) || value
|
|
353
|
+
local_id = [] if remove_non_local_ids && local_id.is_a?(Integer) && local_id > Shard::IDS_PER_SHARD
|
|
354
|
+
local_id
|
|
355
|
+
end
|
|
357
356
|
end
|
|
358
357
|
end
|
|
359
358
|
end
|
|
@@ -1,31 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Switchman
|
|
2
4
|
module ActiveRecord
|
|
3
5
|
module Reflection
|
|
4
|
-
module
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
else
|
|
12
|
-
def join_id_for(owner)
|
|
13
|
-
owner.send(active_record_primary_key) # use sharded id values in association binds
|
|
6
|
+
module AbstractReflection
|
|
7
|
+
def shard(owner)
|
|
8
|
+
if polymorphic? || klass.shard_category == owner.class.shard_category
|
|
9
|
+
# polymorphic associations assume the same shard as the owning item
|
|
10
|
+
owner.shard
|
|
11
|
+
else
|
|
12
|
+
Shard.default
|
|
14
13
|
end
|
|
15
14
|
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module AssociationScopeCache
|
|
18
|
+
def initialize(*args)
|
|
19
|
+
super
|
|
20
|
+
# on ThroughReflection, these won't be initialized (cause it doesn't
|
|
21
|
+
# inherit from AssociationReflection), so make sure they're
|
|
22
|
+
# initialized here
|
|
23
|
+
@association_scope_cache ||= {}
|
|
24
|
+
@scope_lock ||= Mutex.new
|
|
25
|
+
end
|
|
16
26
|
|
|
17
27
|
# cache association scopes by shard.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
key = [key, owner.
|
|
25
|
-
@association_scope_cache[key] ||= @scope_lock.synchronize {
|
|
26
|
-
@association_scope_cache[key] ||= yield
|
|
27
|
-
}
|
|
28
|
+
# this technically belongs on AssociationReflection, but we put it on
|
|
29
|
+
# ThroughReflection as well, instead of delegating to its internal
|
|
30
|
+
# HasManyAssociation, losing its proper `klass`
|
|
31
|
+
def association_scope_cache(conn, owner, &block)
|
|
32
|
+
key = conn.prepared_statements
|
|
33
|
+
if polymorphic?
|
|
34
|
+
key = [key, owner._read_attribute(@foreign_type)]
|
|
28
35
|
end
|
|
36
|
+
key = [key, shard(owner).id].flatten
|
|
37
|
+
@association_scope_cache[key] ||= @scope_lock.synchronize {
|
|
38
|
+
@association_scope_cache[key] ||= (::Rails.version >= "5.2" ? ::ActiveRecord::StatementCache.create(conn, &block) : block.call)
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
module AssociationReflection
|
|
44
|
+
def join_id_for(owner)
|
|
45
|
+
owner.send(::Rails.version >= "5.2" ? join_foreign_key : active_record_primary_key) # use sharded id values in association binds
|
|
29
46
|
end
|
|
30
47
|
end
|
|
31
48
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Switchman
|
|
2
4
|
module ActiveRecord
|
|
3
5
|
module Relation
|
|
@@ -5,19 +7,19 @@ module Switchman
|
|
|
5
7
|
klass::SINGLE_VALUE_METHODS.concat [ :shard, :shard_source ]
|
|
6
8
|
end
|
|
7
9
|
|
|
8
|
-
def initialize(
|
|
10
|
+
def initialize(*, **)
|
|
9
11
|
super
|
|
10
|
-
self.shard_value = Shard.current(klass ? klass.shard_category : :
|
|
12
|
+
self.shard_value = Shard.current(klass ? klass.shard_category : :primary) unless shard_value
|
|
11
13
|
self.shard_source_value = :implicit unless shard_source_value
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
def clone
|
|
15
17
|
result = super
|
|
16
|
-
result.shard_value = Shard.current(klass ? klass.shard_category : :
|
|
18
|
+
result.shard_value = Shard.current(klass ? klass.shard_category : :primary) unless shard_value
|
|
17
19
|
result
|
|
18
20
|
end
|
|
19
21
|
|
|
20
|
-
def merge(*
|
|
22
|
+
def merge(*)
|
|
21
23
|
relation = super
|
|
22
24
|
if relation.shard_value != self.shard_value && relation.shard_source_value == :implicit
|
|
23
25
|
relation.shard_value = self.shard_value
|
|
@@ -26,15 +28,15 @@ module Switchman
|
|
|
26
28
|
relation
|
|
27
29
|
end
|
|
28
30
|
|
|
29
|
-
def new(
|
|
31
|
+
def new(*, &block)
|
|
30
32
|
primary_shard.activate(klass.shard_category) { super }
|
|
31
33
|
end
|
|
32
34
|
|
|
33
|
-
def create(
|
|
35
|
+
def create(*, &block)
|
|
34
36
|
primary_shard.activate(klass.shard_category) { super }
|
|
35
37
|
end
|
|
36
38
|
|
|
37
|
-
def create!(
|
|
39
|
+
def create!(*, &block)
|
|
38
40
|
primary_shard.activate(klass.shard_category) { super }
|
|
39
41
|
end
|
|
40
42
|
|
|
@@ -42,40 +44,59 @@ module Switchman
|
|
|
42
44
|
primary_shard.activate(klass.shard_category) { super }
|
|
43
45
|
end
|
|
44
46
|
|
|
45
|
-
def explain
|
|
46
|
-
|
|
47
|
-
self.activate { |relation| relation.explain(super_method: true) }
|
|
47
|
+
def explain
|
|
48
|
+
self.activate { |relation| relation.call_super(:explain, Relation) }
|
|
48
49
|
end
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
when Array, ::ActiveRecord::Relation, ::ActiveRecord::Base
|
|
58
|
-
@records = results
|
|
59
|
-
@loaded = true
|
|
60
|
-
end
|
|
61
|
-
results
|
|
51
|
+
def records
|
|
52
|
+
return @records if loaded?
|
|
53
|
+
results = self.activate { |relation| relation.call_super(:records, Relation) }
|
|
54
|
+
case shard_value
|
|
55
|
+
when Array, ::ActiveRecord::Relation, ::ActiveRecord::Base
|
|
56
|
+
@records = results
|
|
57
|
+
@loaded = true
|
|
62
58
|
end
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
CALL_SUPER = Object.new.freeze
|
|
66
|
-
private_constant :CALL_SUPER
|
|
59
|
+
results
|
|
60
|
+
end
|
|
67
61
|
|
|
68
|
-
%
|
|
62
|
+
%I{update_all delete_all}.each do |method|
|
|
69
63
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
70
64
|
def #{method}(*args)
|
|
71
|
-
|
|
72
|
-
result = self.activate { |relation| relation.#{method}(CALL_SUPER, *args) }
|
|
65
|
+
result = self.activate { |relation| relation.call_super(#{method.inspect}, Relation, *args) }
|
|
73
66
|
result = result.sum if result.is_a?(Array)
|
|
74
67
|
result
|
|
75
68
|
end
|
|
76
69
|
RUBY
|
|
77
70
|
end
|
|
78
71
|
|
|
72
|
+
def find_ids_in_ranges(options = {})
|
|
73
|
+
is_integer = columns_hash[primary_key.to_s].type == :integer
|
|
74
|
+
loose_mode = options[:loose] && is_integer
|
|
75
|
+
# loose_mode: if we don't care about getting exactly batch_size ids in between
|
|
76
|
+
# don't get the max - just get the min and add batch_size so we get that many _at most_
|
|
77
|
+
values = loose_mode ? "MIN(id)" : "MIN(id), MAX(id)"
|
|
78
|
+
|
|
79
|
+
batch_size = options[:batch_size].try(:to_i) || 1000
|
|
80
|
+
quoted_primary_key = "#{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).select("#{quoted_primary_key}#{as_id}").reorder(primary_key.to_sym).limit(loose_mode ? 1 : batch_size)
|
|
83
|
+
subquery_scope = subquery_scope.where("#{quoted_primary_key} <= ?", options[:end_at]) if options[:end_at]
|
|
84
|
+
|
|
85
|
+
first_subquery_scope = options[:start_at] ? subquery_scope.where("#{quoted_primary_key} >= ?", options[:start_at]) : subquery_scope
|
|
86
|
+
|
|
87
|
+
ids = connection.select_rows("SELECT #{values} FROM (#{first_subquery_scope.to_sql}) AS subquery").first
|
|
88
|
+
|
|
89
|
+
while ids.first.present?
|
|
90
|
+
ids.map!(&:to_i) if is_integer
|
|
91
|
+
ids << ids.first + batch_size if loose_mode
|
|
92
|
+
|
|
93
|
+
yield(*ids)
|
|
94
|
+
last_value = ids.last
|
|
95
|
+
next_subquery_scope = subquery_scope.where(["#{quoted_primary_key}>?", last_value])
|
|
96
|
+
ids = connection.select_rows("SELECT #{values} FROM (#{next_subquery_scope.to_sql}) AS subquery").first
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
79
100
|
def activate(&block)
|
|
80
101
|
shards = all_shards
|
|
81
102
|
if (Array === shards && shards.length == 1)
|