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.
- checksums.yaml +4 -4
- data/Rakefile +16 -15
- 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/action_controller/caching.rb +2 -2
- data/lib/switchman/active_record/abstract_adapter.rb +6 -6
- data/lib/switchman/active_record/associations.rb +365 -0
- data/lib/switchman/active_record/attribute_methods.rb +188 -99
- data/lib/switchman/active_record/base.rb +185 -40
- data/lib/switchman/active_record/calculations.rb +64 -40
- data/lib/switchman/active_record/connection_handler.rb +18 -0
- data/lib/switchman/active_record/connection_pool.rb +24 -5
- data/lib/switchman/active_record/database_configurations.rb +37 -13
- data/lib/switchman/active_record/finder_methods.rb +46 -16
- data/lib/switchman/active_record/log_subscriber.rb +11 -5
- data/lib/switchman/active_record/migration.rb +52 -8
- data/lib/switchman/active_record/model_schema.rb +1 -1
- data/lib/switchman/active_record/persistence.rb +31 -3
- data/lib/switchman/active_record/postgresql_adapter.rb +11 -10
- data/lib/switchman/active_record/predicate_builder.rb +2 -2
- data/lib/switchman/active_record/query_cache.rb +49 -20
- data/lib/switchman/active_record/query_methods.rb +187 -136
- data/lib/switchman/active_record/reflection.rb +1 -1
- data/lib/switchman/active_record/relation.rb +33 -26
- data/lib/switchman/active_record/spawn_methods.rb +2 -2
- data/lib/switchman/active_record/statement_cache.rb +11 -7
- 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 +26 -16
- data/lib/switchman/active_support/cache.rb +20 -1
- data/lib/switchman/arel.rb +34 -18
- data/lib/switchman/call_super.rb +8 -2
- data/lib/switchman/database_server.rb +91 -45
- data/lib/switchman/default_shard.rb +14 -5
- data/lib/switchman/engine.rb +79 -126
- data/lib/switchman/environment.rb +2 -2
- data/lib/switchman/errors.rb +17 -2
- data/lib/switchman/guard_rail/relation.rb +8 -10
- data/lib/switchman/guard_rail.rb +5 -0
- data/lib/switchman/parallel.rb +68 -0
- data/lib/switchman/r_spec_helper.rb +14 -11
- data/lib/switchman/rails.rb +2 -5
- data/{app/models → lib}/switchman/shard.rb +186 -189
- data/lib/switchman/sharded_instrumenter.rb +5 -1
- data/lib/switchman/shared_schema_cache.rb +11 -0
- data/lib/switchman/standard_error.rb +6 -5
- data/lib/switchman/test_helper.rb +2 -2
- data/{app/models → lib}/switchman/unsharded_record.rb +1 -1
- data/lib/switchman/version.rb +1 -1
- data/lib/switchman.rb +44 -12
- data/lib/tasks/switchman.rake +74 -53
- metadata +42 -53
- data/lib/switchman/active_record/association.rb +0 -206
- 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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
106
|
+
self
|
|
109
107
|
end
|
|
110
108
|
|
|
111
|
-
|
|
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::
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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 ==
|
|
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.
|
|
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.
|
|
196
|
+
return Shard.current(klass.connection_class_for_self) if reflection.options[:polymorphic]
|
|
197
197
|
|
|
198
|
-
Shard.current(reflection.klass.
|
|
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)
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
else
|
|
302
|
-
predicate.right
|
|
303
|
-
end
|
|
257
|
+
new_predicates = old_predicates.map(&block)
|
|
258
|
+
return if new_predicates == old_predicates
|
|
304
259
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
314
|
-
predicate
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
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
|
|
324
|
-
klass =
|
|
325
|
-
klass.new(
|
|
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
|
-
|
|
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
|
-
|
|
377
|
+
yield(node)
|
|
331
378
|
end
|
|
332
379
|
end
|
|
333
380
|
|
|
334
|
-
def
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
393
|
+
primary_shard
|
|
350
394
|
end
|
|
351
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
52
|
-
|
|
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
|
-
|
|
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}(
|
|
66
|
-
result = self.activate(unordered: true) { |relation| relation.call_super(#{method.inspect}, Relation,
|
|
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 ?
|
|
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 =
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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]
|
|
@@ -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)
|
|
@@ -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.
|
|
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.
|
|
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::
|
|
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
|