switchman 2.0.5 → 2.0.10

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 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: []