switchman 2.0.5 → 2.0.10

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c544d2ed8498a6aecf33823f39b304393a65f213f6a68bf04f4d36610ebe3ac2
4
- data.tar.gz: be685b315997b103dd13376f5223196f1b66a5be490502aac6bc401408899abc
3
+ metadata.gz: bce5f0819ee2570b0bf51ba3872af1f34c23273712cc33c35559225723fb13a0
4
+ data.tar.gz: 11e7c3d612263f94ea17bc5213580ae42e2c12743a13ff36e9f988feddda0a7e
5
5
  SHA512:
6
- metadata.gz: fbdd769d90c831e1bbe6dd39def12775537989346daae7dbbab3103a330b89151e9d1c3b9fe216ab9ade75e3c01ee1b1c91062cc622707c3c2697f7dc3239ea8
7
- data.tar.gz: 27d32eefa842a76debb1a1fc7875407b4df1398a45a71419c7cc13045fd8705ff5e4af724478a9cb854d517433ae8d16420baa13fc8e7459ab1953e626c0c80a
6
+ metadata.gz: 0fad57a01e86f34533c5cf539cf92360605b129bdfdf77878fec72663c3783bad56e7a7a324f3b388e37a89296d05f309dc2c84084e30c1c44d20917e15d35e3
7
+ data.tar.gz: 002bcec40325a407c8be94432d2396c8ae812ad2828695266e591956d1e1e8195628906fce710c4681a3c72cc5f6ea8f1859a99aac22cbd3462be5dd6118bbdf
@@ -718,6 +718,7 @@ module Switchman
718
718
  Switchman.cache.delete(['shard', id].join('/'))
719
719
  Switchman.cache.delete("default_shard") if default?
720
720
  end
721
+ self.class.clear_cache
721
722
  end
722
723
 
723
724
  def default_name
@@ -10,6 +10,6 @@ class AddDefaultShardIndex < ActiveRecord::Migration[4.2]
10
10
  else
11
11
  {}
12
12
  end
13
- add_index :switchman_shards, :default, options
13
+ add_index :switchman_shards, :default, **options
14
14
  end
15
15
  end
data/lib/switchman.rb CHANGED
@@ -17,4 +17,6 @@ module Switchman
17
17
  def self.cache=(cache)
18
18
  @cache = cache
19
19
  end
20
+
21
+ class OrderOnMultiShardQuery < RuntimeError; end
20
22
  end
@@ -87,7 +87,7 @@ module Switchman
87
87
  # Copypasta from Activerecord but with added global_id_for goodness.
88
88
  def records_for(ids)
89
89
  scope.where(association_key_name => ids).load do |record|
90
- global_key = if record.class.shard_category == :unsharded
90
+ global_key = if model.shard_category == :unsharded
91
91
  convert_key(record[association_key_name])
92
92
  else
93
93
  Shard.global_id_for(record[association_key_name], record.shard)
@@ -51,7 +51,7 @@ module Switchman
51
51
 
52
52
  def calculate_simple_average(column_name, distinct)
53
53
  # See activerecord#execute_simple_calculation
54
- relation = reorder(nil)
54
+ relation = except(:order)
55
55
  column = aggregate_column(column_name)
56
56
  relation.select_values = [operation_over_aggregate_column(column, "average", distinct).as("average"),
57
57
  operation_over_aggregate_column(column, "count", distinct).as("count")]
@@ -135,7 +135,7 @@ module Switchman
135
135
  primary_pool = retrieve_connection_pool("primary")
136
136
  if primary_pool.is_a?(ConnectionPoolProxy)
137
137
  pool = ConnectionPoolProxy.new(spec_name.to_sym, primary_pool.default_pool, @shard_connection_pools)
138
- pool.schema_cache.copy_values(primary_pool.schema_cache)
138
+ pool.schema_cache.copy_references(primary_pool.schema_cache)
139
139
  pool
140
140
  else
141
141
  primary_pool
@@ -30,10 +30,42 @@ module Switchman
30
30
  end
31
31
 
32
32
  module Migrator
33
+ # significant change: hash shard id, not database name
33
34
  def generate_migrator_advisory_lock_id
34
- shard_name_hash = Zlib.crc32("#{Shard.current.id}:#{Shard.current.name}")
35
+ shard_name_hash = Zlib.crc32(Shard.current.name)
35
36
  ::ActiveRecord::Migrator::MIGRATOR_SALT * shard_name_hash
36
37
  end
38
+
39
+ if ::Rails.version >= '6.0'
40
+ # copy/paste from Rails 6.1
41
+ def with_advisory_lock
42
+ lock_id = generate_migrator_advisory_lock_id
43
+
44
+ with_advisory_lock_connection do |connection|
45
+ got_lock = connection.get_advisory_lock(lock_id)
46
+ raise ::ActiveRecord::ConcurrentMigrationError unless got_lock
47
+ load_migrated # reload schema_migrations to be sure it wasn't changed by another process before we got the lock
48
+ yield
49
+ ensure
50
+ if got_lock && !connection.release_advisory_lock(lock_id)
51
+ raise ::ActiveRecord::ConcurrentMigrationError.new(
52
+ ::ActiveRecord::ConcurrentMigrationError::RELEASE_LOCK_FAILED_MESSAGE
53
+ )
54
+ end
55
+ end
56
+ end
57
+
58
+ # significant change: strip out prefer_secondary from config
59
+ def with_advisory_lock_connection
60
+ pool = ::ActiveRecord::ConnectionAdapters::ConnectionHandler.new.establish_connection(
61
+ ::ActiveRecord::Base.connection_config.except(:prefer_secondary)
62
+ )
63
+
64
+ pool.with_connection { |connection| yield(connection) }
65
+ ensure
66
+ pool&.disconnect!
67
+ end
68
+ end
37
69
  end
38
70
 
39
71
  module MigrationContext
@@ -40,14 +40,6 @@ module Switchman
40
40
  select_values("SELECT * FROM unnest(current_schemas(false))")
41
41
  end
42
42
 
43
- def tables(name = nil)
44
- query(<<-SQL, 'SCHEMA').map { |row| row[0] }
45
- SELECT tablename
46
- FROM pg_tables
47
- WHERE schemaname = '#{shard.name}'
48
- SQL
49
- end
50
-
51
43
  def extract_schema_qualified_name(string)
52
44
  name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(string.to_s)
53
45
  if string && !name.schema
@@ -56,80 +48,155 @@ module Switchman
56
48
  [name.schema, name.identifier]
57
49
  end
58
50
 
59
- def view_exists?(name)
60
- name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(name.to_s)
61
- return false unless name.identifier
62
- if !name.schema
63
- name.instance_variable_set(:@schema, shard.name)
64
- end
65
-
66
- select_values(<<-SQL, 'SCHEMA').any?
67
- SELECT c.relname
68
- FROM pg_class c
69
- LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
70
- WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view
71
- AND c.relname = '#{name.identifier}'
72
- AND n.nspname = '#{shard.name}'
73
- SQL
51
+ # significant change: use the shard name if no explicit schema
52
+ def quoted_scope(name = nil, type: nil)
53
+ schema, name = extract_schema_qualified_name(name)
54
+ type = \
55
+ case type
56
+ when "BASE TABLE"
57
+ "'r','p'"
58
+ when "VIEW"
59
+ "'v','m'"
60
+ when "FOREIGN TABLE"
61
+ "'f'"
62
+ end
63
+ scope = {}
64
+ scope[:schema] = quote(schema || shard.name)
65
+ scope[:name] = quote(name) if name
66
+ scope[:type] = type if type
67
+ scope
74
68
  end
75
69
 
76
- def indexes(table_name)
77
- result = query(<<-SQL, 'SCHEMA')
78
- SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid
79
- FROM pg_class t
80
- INNER JOIN pg_index d ON t.oid = d.indrelid
81
- INNER JOIN pg_class i ON d.indexrelid = i.oid
82
- WHERE i.relkind = 'i'
83
- AND d.indisprimary = 'f'
84
- AND t.relname = '#{table_name}'
85
- AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = '#{shard.name}' )
86
- ORDER BY i.relname
87
- SQL
88
-
89
-
90
- result.map do |row|
91
- index_name = row[0]
92
- unique = row[1] == true || row[1] == 't'
93
- indkey = row[2].split(" ")
94
- inddef = row[3]
95
- oid = row[4]
96
-
97
- columns = Hash[query(<<-SQL, "SCHEMA")]
98
- SELECT a.attnum, a.attname
99
- FROM pg_attribute a
100
- WHERE a.attrelid = #{oid}
101
- AND a.attnum IN (#{indkey.join(",")})
70
+ if ::Rails.version < '6.0'
71
+ def tables(name = nil)
72
+ query(<<-SQL, 'SCHEMA').map { |row| row[0] }
73
+ SELECT tablename
74
+ FROM pg_tables
75
+ WHERE schemaname = '#{shard.name}'
102
76
  SQL
77
+ end
103
78
 
104
- column_names = columns.stringify_keys.values_at(*indkey).compact
105
-
106
- unless column_names.empty?
107
- # add info on sort order for columns (only desc order is explicitly specified, asc is the default)
108
- desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
109
- orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {}
110
- where = inddef.scan(/WHERE (.+)$/).flatten[0]
111
- using = inddef.scan(/USING (.+?) /).flatten[0].to_sym
112
-
113
- if ::Rails.version >= "5.2"
114
- ::ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, index_name, unique, column_names, orders: orders, where: where, using: using)
115
- else
116
- ::ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using)
117
- end
79
+ def view_exists?(name)
80
+ name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(name.to_s)
81
+ return false unless name.identifier
82
+ if !name.schema
83
+ name.instance_variable_set(:@schema, shard.name)
118
84
  end
119
- end.compact
120
- end
121
85
 
122
- def index_name_exists?(table_name, index_name, _default = nil)
123
- exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
124
- SELECT COUNT(*)
86
+ select_values(<<-SQL, 'SCHEMA').any?
87
+ SELECT c.relname
88
+ FROM pg_class c
89
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
90
+ WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view
91
+ AND c.relname = '#{name.identifier}'
92
+ AND n.nspname = '#{shard.name}'
93
+ SQL
94
+ end
95
+
96
+ def indexes(table_name)
97
+ result = query(<<-SQL, 'SCHEMA')
98
+ SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid
125
99
  FROM pg_class t
126
100
  INNER JOIN pg_index d ON t.oid = d.indrelid
127
101
  INNER JOIN pg_class i ON d.indexrelid = i.oid
128
102
  WHERE i.relkind = 'i'
129
- AND i.relname = '#{index_name}'
103
+ AND d.indisprimary = 'f'
130
104
  AND t.relname = '#{table_name}'
131
105
  AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = '#{shard.name}' )
132
- SQL
106
+ ORDER BY i.relname
107
+ SQL
108
+
109
+
110
+ result.map do |row|
111
+ index_name = row[0]
112
+ unique = row[1] == true || row[1] == 't'
113
+ indkey = row[2].split(" ")
114
+ inddef = row[3]
115
+ oid = row[4]
116
+
117
+ columns = Hash[query(<<-SQL, "SCHEMA")]
118
+ SELECT a.attnum, a.attname
119
+ FROM pg_attribute a
120
+ WHERE a.attrelid = #{oid}
121
+ AND a.attnum IN (#{indkey.join(",")})
122
+ SQL
123
+
124
+ column_names = columns.stringify_keys.values_at(*indkey).compact
125
+
126
+ unless column_names.empty?
127
+ # add info on sort order for columns (only desc order is explicitly specified, asc is the default)
128
+ desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
129
+ orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {}
130
+ where = inddef.scan(/WHERE (.+)$/).flatten[0]
131
+ using = inddef.scan(/USING (.+?) /).flatten[0].to_sym
132
+
133
+ if ::Rails.version >= "5.2"
134
+ ::ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, index_name, unique, column_names, orders: orders, where: where, using: using)
135
+ else
136
+ ::ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using)
137
+ end
138
+ end
139
+ end.compact
140
+ end
141
+
142
+ def index_name_exists?(table_name, index_name, _default = nil)
143
+ exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
144
+ SELECT COUNT(*)
145
+ FROM pg_class t
146
+ INNER JOIN pg_index d ON t.oid = d.indrelid
147
+ INNER JOIN pg_class i ON d.indexrelid = i.oid
148
+ WHERE i.relkind = 'i'
149
+ AND i.relname = '#{index_name}'
150
+ AND t.relname = '#{table_name}'
151
+ AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = '#{shard.name}' )
152
+ SQL
153
+ end
154
+
155
+ def foreign_keys(table_name)
156
+ # mostly copy-pasted from AR - only change is to the nspname condition for qualified names support
157
+ fk_info = select_all <<-SQL.strip_heredoc
158
+ SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete
159
+ FROM pg_constraint c
160
+ JOIN pg_class t1 ON c.conrelid = t1.oid
161
+ JOIN pg_class t2 ON c.confrelid = t2.oid
162
+ JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
163
+ JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
164
+ JOIN pg_namespace t3 ON c.connamespace = t3.oid
165
+ WHERE c.contype = 'f'
166
+ AND t1.relname = #{quote(table_name)}
167
+ AND t3.nspname = '#{shard.name}'
168
+ ORDER BY c.conname
169
+ SQL
170
+
171
+ fk_info.map do |row|
172
+ options = {
173
+ column: row['column'],
174
+ name: row['name'],
175
+ primary_key: row['primary_key']
176
+ }
177
+
178
+ options[:on_delete] = extract_foreign_key_action(row['on_delete'])
179
+ options[:on_update] = extract_foreign_key_action(row['on_update'])
180
+
181
+ # strip the schema name from to_table if it matches
182
+ to_table = row['to_table']
183
+ to_table_qualified_name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(to_table)
184
+ if to_table_qualified_name.schema == shard.name
185
+ to_table = to_table_qualified_name.identifier
186
+ end
187
+
188
+ ::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(table_name, to_table, options)
189
+ end
190
+ end
191
+ else
192
+ def foreign_keys(table_name)
193
+ super.each do |fk|
194
+ to_table_qualified_name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(fk.to_table)
195
+ if to_table_qualified_name.schema == shard.name
196
+ fk.to_table = to_table_qualified_name.identifier
197
+ end
198
+ end
199
+ end
133
200
  end
134
201
 
135
202
  def quote_local_table_name(name)
@@ -156,44 +223,6 @@ module Switchman
156
223
  @use_local_table_name = old_value
157
224
  end
158
225
 
159
- def foreign_keys(table_name)
160
-
161
- # mostly copy-pasted from AR - only change is to the nspname condition for qualified names support
162
- fk_info = select_all <<-SQL.strip_heredoc
163
- SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete
164
- FROM pg_constraint c
165
- JOIN pg_class t1 ON c.conrelid = t1.oid
166
- JOIN pg_class t2 ON c.confrelid = t2.oid
167
- JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
168
- JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
169
- JOIN pg_namespace t3 ON c.connamespace = t3.oid
170
- WHERE c.contype = 'f'
171
- AND t1.relname = #{quote(table_name)}
172
- AND t3.nspname = '#{shard.name}'
173
- ORDER BY c.conname
174
- SQL
175
-
176
- fk_info.map do |row|
177
- options = {
178
- column: row['column'],
179
- name: row['name'],
180
- primary_key: row['primary_key']
181
- }
182
-
183
- options[:on_delete] = extract_foreign_key_action(row['on_delete'])
184
- options[:on_update] = extract_foreign_key_action(row['on_update'])
185
-
186
- # strip the schema name from to_table if it matches
187
- to_table = row['to_table']
188
- to_table_qualified_name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(to_table)
189
- if to_table_qualified_name.schema == shard.name
190
- to_table = to_table_qualified_name.identifier
191
- end
192
-
193
- ::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(table_name, to_table, options)
194
- end
195
- end
196
-
197
226
  def add_index_options(_table_name, _column_name, **)
198
227
  index_name, index_type, index_columns, index_options, algorithm, using = super
199
228
  algorithm = nil if DatabaseServer.creating_new_shard && algorithm == "CONCURRENTLY"
@@ -78,10 +78,6 @@ module Switchman
78
78
  end
79
79
  end
80
80
 
81
- def or(other)
82
- super(other.shard(self.primary_shard))
83
- end
84
-
85
81
  private
86
82
 
87
83
  if ::Rails.version >= '5.2'
@@ -246,135 +242,92 @@ module Switchman
246
242
  binds: nil,
247
243
  dup_binds_on_mutation: false)
248
244
  result = predicates.map do |predicate|
249
- transposed, binds = transpose_single_predicate(predicate, source_shard, target_shard, remove_nonlocal_primary_keys,
250
- binds: binds, dup_binds_on_mutation: dup_binds_on_mutation)
251
- transposed
252
- end
253
- result = [result, binds]
254
- result
255
- end
245
+ next predicate unless predicate.is_a?(::Arel::Nodes::Binary)
246
+ next predicate unless predicate.left.is_a?(::Arel::Attributes::Attribute)
247
+ relation, column = relation_and_column(predicate.left)
248
+ next predicate unless (type = transposable_attribute_type(relation, column))
256
249
 
257
- def transpose_single_predicate(predicate,
258
- source_shard,
259
- target_shard,
260
- remove_nonlocal_primary_keys = false,
261
- binds: nil,
262
- dup_binds_on_mutation: false)
263
- if predicate.is_a?(::Arel::Nodes::Grouping)
264
- return predicate, binds unless predicate.expr.is_a?(::Arel::Nodes::Or)
265
- # Dang, we have an OR. OK, that means we have other epxressions below this
266
- # level, perhaps many, that may need transposition.
267
- # the left side and right side must each be treated as predicate lists and
268
- # transformed in kind, if neither of them changes we can just return the grouping as is.
269
- # hold on, it's about to get recursive...
270
- #
271
- # TODO: "binds" is getting passed up and down
272
- # this stack purely because of the necessary handling for rails <5.2
273
- # Dropping support for 5.2 means we can remove the "binds" argument from
274
- # all of this and yank the conditional below where we monkey with their instance state.
275
- or_expr = predicate.expr
276
- left_node = or_expr.left
277
- right_node = or_expr.right
278
- left_predicates = left_node.children
279
- right_predicates = right_node.children
280
- new_left_predicates, binds = transpose_predicates(left_predicates, source_shard,
281
- target_shard, remove_nonlocal_primary_keys,
282
- binds: binds, dup_binds_on_mutation: dup_binds_on_mutation)
283
- new_right_predicates, binds = transpose_predicates(right_predicates, source_shard,
284
- target_shard, remove_nonlocal_primary_keys,
285
- binds: binds, dup_binds_on_mutation: dup_binds_on_mutation)
286
- if new_left_predicates != left_predicates
287
- left_node.instance_variable_set(:@children, new_left_predicates)
288
- end
289
- if new_right_predicates != right_predicates
290
- right_node.instance_variable_set(:@children, new_right_predicates)
291
- end
292
- return predicate, binds
293
- end
294
- return predicate, binds unless predicate.is_a?(::Arel::Nodes::Binary)
295
- return predicate, binds unless predicate.left.is_a?(::Arel::Attributes::Attribute)
296
- relation, column = relation_and_column(predicate.left)
297
- return predicate, binds unless (type = transposable_attribute_type(relation, column))
250
+ remove = true if type == :primary &&
251
+ remove_nonlocal_primary_keys &&
252
+ predicate.left.relation.model == klass &&
253
+ (predicate.is_a?(::Arel::Nodes::Equality) || predicate.is_a?(::Arel::Nodes::In))
298
254
 
299
- remove = true if type == :primary &&
300
- remove_nonlocal_primary_keys &&
301
- predicate.left.relation.model == klass &&
302
- predicate.is_a?(::Arel::Nodes::Equality)
303
-
304
- current_source_shard =
305
- if source_shard
306
- source_shard
307
- elsif type == :primary
308
- Shard.current(klass.shard_category)
309
- elsif type == :foreign
310
- source_shard_for_foreign_key(relation, column)
311
- end
255
+ current_source_shard =
256
+ if source_shard
257
+ source_shard
258
+ elsif type == :primary
259
+ Shard.current(klass.shard_category)
260
+ elsif type == :foreign
261
+ source_shard_for_foreign_key(relation, column)
262
+ end
312
263
 
313
- if ::Rails.version >= "5.2"
314
- new_right_value =
315
- case predicate.right
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).presence }.compact
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
316
274
  when Array
317
- predicate.right.map {|val| transpose_predicate_value(val, current_source_shard, target_shard, type, remove) }
318
- else
319
- transpose_predicate_value(predicate.right, current_source_shard, target_shard, type, remove)
320
- end
321
- else
322
- new_right_value = case predicate.right
323
- when Array
324
- local_ids = []
325
- predicate.right.each do |value|
326
- local_id = Shard.relative_id_for(value, current_source_shard, target_shard)
327
- next unless local_id
328
- unless remove && local_id > Shard::IDS_PER_SHARD
329
- if value.is_a?(::Arel::Nodes::Casted)
330
- if local_id == value.val
331
- local_id = value
332
- elsif local_id != value
333
- local_id = value.class.new(local_id, value.attribute)
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
334
286
  end
287
+ local_ids << local_id
335
288
  end
336
- local_ids << local_id
337
- end
338
- end
339
- local_ids
340
- when ::Arel::Nodes::BindParam
341
- # look for a bind param with a matching column name
342
- if binds && bind = binds.detect{|b| b&.name.to_s == predicate.left.name.to_s}
343
- # before we mutate, dup
344
- if dup_binds_on_mutation
345
- binds = binds.map(&:dup)
346
- dup_binds_on_mutation = false
347
- bind = binds.find { |b| b&.name.to_s == predicate.left.name.to_s }
348
289
  end
349
- if bind.value.is_a?(::ActiveRecord::StatementCache::Substitute)
350
- bind.value.sharded = true # mark for transposition later
351
- bind.value.primary = true if type == :primary
352
- else
353
- local_id = Shard.relative_id_for(bind.value, current_source_shard, target_shard)
354
- local_id = [] if remove && local_id > Shard::IDS_PER_SHARD
355
- bind.instance_variable_set(:@value, local_id)
356
- bind.instance_variable_set(:@value_for_database, nil)
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
300
+ if bind.value.is_a?(::ActiveRecord::StatementCache::Substitute)
301
+ bind.value.sharded = true # mark for transposition later
302
+ bind.value.primary = true if type == :primary
303
+ else
304
+ local_id = Shard.relative_id_for(bind.value, current_source_shard, target_shard)
305
+ local_id = [] if remove && local_id > Shard::IDS_PER_SHARD
306
+ bind.instance_variable_set(:@value, local_id)
307
+ bind.instance_variable_set(:@value_for_database, nil)
308
+ end
357
309
  end
310
+ predicate.right
311
+ else
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
358
315
  end
359
- predicate.right
360
- else
361
- local_id = Shard.relative_id_for(predicate.right, current_source_shard, target_shard) || predicate.right
362
- local_id = [] if remove && local_id.is_a?(Integer) && local_id > Shard::IDS_PER_SHARD
363
- local_id
364
316
  end
365
- end
366
- out_predicate = if new_right_value == predicate.right
367
- predicate
368
- elsif predicate.right.is_a?(::Arel::Nodes::Casted)
369
- if new_right_value == predicate.right.val
317
+ if new_right_value == predicate.right
370
318
  predicate
319
+ elsif predicate.right.is_a?(::Arel::Nodes::Casted)
320
+ if new_right_value == predicate.right.val
321
+ predicate
322
+ else
323
+ predicate.class.new(predicate.left, predicate.right.class.new(new_right_value, predicate.right.attribute))
324
+ end
371
325
  else
372
- predicate.class.new(predicate.left, predicate.right.class.new(new_right_value, predicate.right.attribute))
326
+ predicate.class.new(predicate.left, new_right_value)
373
327
  end
374
- else
375
- predicate.class.new(predicate.left, new_right_value)
376
328
  end
377
- return out_predicate, binds
329
+ result = [result, binds]
330
+ result
378
331
  end
379
332
 
380
333
  def transpose_predicate_value(value, current_shard, target_shard, attribute_type, remove_non_local_ids)
@@ -387,7 +340,7 @@ module Switchman
387
340
  value
388
341
  else
389
342
  local_id = Shard.relative_id_for(current_id, current_shard, target_shard) || current_id
390
- local_id = [] if remove_non_local_ids && local_id.is_a?(Integer) && local_id > Shard::IDS_PER_SHARD
343
+ return nil if remove_non_local_ids && local_id.is_a?(Integer) && local_id > Shard::IDS_PER_SHARD
391
344
  if current_id != local_id
392
345
  # make a new bind param
393
346
  ::Arel::Nodes::BindParam.new(query_att.class.new(query_att.name, local_id, query_att.type))
@@ -106,10 +106,62 @@ module Switchman
106
106
  shards.first.activate(klass.shard_category) { yield(self, shards.first) }
107
107
  end
108
108
  else
109
- # TODO: implement local limit to avoid querying extra shards
110
- Shard.with_each_shard(shards, [klass.shard_category]) do
111
- shard(Shard.current(klass.shard_category), :to_a).activate(&block)
109
+ result_count = 0
110
+ can_order = false
111
+ result = Shard.with_each_shard(shards, [klass.shard_category]) do
112
+ # don't even query other shards if we're already past the limit
113
+ next if limit_value && result_count >= limit_value && order_values.empty?
114
+
115
+ relation = shard(Shard.current(klass.shard_category), :to_a)
116
+ # do a minimal query if possible
117
+ relation = relation.limit(limit_value - result_count) if limit_value && !result_count.zero? && order_values.empty?
118
+
119
+ shard_results = relation.activate(&block)
120
+
121
+ if shard_results.present?
122
+ can_order ||= can_order_cross_shard_results? unless order_values.empty?
123
+ raise OrderOnMultiShardQuery if !can_order && !order_values.empty? && result_count.positive?
124
+
125
+ result_count += shard_results.is_a?(Array) ? shard_results.length : 1
126
+ end
127
+ shard_results
128
+ end
129
+
130
+ result = reorder_cross_shard_results(result) if can_order
131
+ result.slice!(limit_value..-1) if limit_value
132
+ result
133
+ end
134
+ end
135
+
136
+ def can_order_cross_shard_results?
137
+ # we only presume to be able to post-sort the most basic of orderings
138
+ order_values.all? { |ov| ov.is_a?(::Arel::Nodes::Ordering) && ov.expr.is_a?(::Arel::Attributes::Attribute) }
139
+ end
140
+
141
+ def reorder_cross_shard_results(results)
142
+ results.sort! do |l, r|
143
+ result = 0
144
+ order_values.each do |ov|
145
+ if l.respond_to?(ov.expr.name)
146
+ a = l.send(ov.expr.name)
147
+ b = r.send(ov.expr.name)
148
+ else
149
+ a = l.attributes[ov.expr.name]
150
+ b = r.attributes[ov.expr.name]
151
+ end
152
+ next if a == b
153
+
154
+ if a.nil? || b.nil?
155
+ result = 1 if a.nil?
156
+ result *= -1 if ov.is_a?(::Arel::Nodes::Descending)
157
+ else
158
+ result = a <=> b
159
+ end
160
+
161
+ result *= -1 if ov.is_a?(::Arel::Nodes::Descending)
162
+ break unless result.zero?
112
163
  end
164
+ result
113
165
  end
114
166
  end
115
167
  end
@@ -84,6 +84,10 @@ module Switchman
84
84
  @schema_cache
85
85
  end
86
86
 
87
+ def set_schema_cache(cache)
88
+ @schema_cache.copy_values(cache)
89
+ end
90
+
87
91
  %w{release_connection
88
92
  disconnect!
89
93
  flush!
@@ -5,14 +5,22 @@ module Switchman
5
5
  delegate :connection, to: :pool
6
6
  attr_reader :pool
7
7
 
8
+ SHARED_IVS = %i{@columns @columns_hash @primary_keys @data_sources @indexes}.freeze
9
+
8
10
  def initialize(pool)
9
11
  @pool = pool
10
12
  super(nil)
11
13
  end
12
14
 
13
15
  def copy_values(other_cache)
16
+ SHARED_IVS.each do |iv|
17
+ instance_variable_get(iv).replace(other_cache.instance_variable_get(iv))
18
+ end
19
+ end
20
+
21
+ def copy_references(other_cache)
14
22
  # use the same cached values but still fall back to the correct pool
15
- [:@columns, :@columns_hash, :@primary_keys, :@data_sources].each do |iv|
23
+ SHARED_IVS.each do |iv|
16
24
  instance_variable_set(iv, other_cache.instance_variable_get(iv))
17
25
  end
18
26
  end
@@ -8,7 +8,7 @@ module Switchman
8
8
  end
9
9
 
10
10
  def current_shard(category = :primary)
11
- @active_shards[category] || Shard.default
11
+ @active_shards&.[](category) || Shard.default
12
12
  end
13
13
  end
14
14
  end
@@ -24,7 +24,7 @@ module Switchman
24
24
  else
25
25
  server1 = Shard.default.database_server
26
26
  end
27
- server2 = DatabaseServer.create(Shard.default.database_server.config)
27
+ server2 = DatabaseServer.create(Shard.default.database_server.config.merge(server2: true))
28
28
 
29
29
  if server1 == Shard.default.database_server && server1.config[:shard1] && server1.config[:shard2]
30
30
  # look for the shards in the db already
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Switchman
4
- VERSION = "2.0.5"
4
+ VERSION = "2.0.10"
5
5
  end
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: switchman
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.5
4
+ version: 2.0.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody Cutrer
8
8
  - James Williams
9
9
  - Jacob Fugal
10
- autorequire:
10
+ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2021-03-02 00:00:00.000000000 Z
13
+ date: 2021-06-09 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: railties
@@ -257,7 +257,7 @@ homepage: http://www.instructure.com/
257
257
  licenses:
258
258
  - MIT
259
259
  metadata: {}
260
- post_install_message:
260
+ post_install_message:
261
261
  rdoc_options: []
262
262
  require_paths:
263
263
  - lib
@@ -272,8 +272,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
272
272
  - !ruby/object:Gem::Version
273
273
  version: '0'
274
274
  requirements: []
275
- rubygems_version: 3.0.3
276
- signing_key:
275
+ rubygems_version: 3.2.15
276
+ signing_key:
277
277
  specification_version: 4
278
278
  summary: Rails sharding magic
279
279
  test_files: []