switchman 3.0.0 → 3.0.5

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: bb89193fead8142e30280a6ab2706f6e9af798ff18820fa144be3dd9ac77591f
4
- data.tar.gz: e1157f5656f06ae9bdb7c62a2c9ae1611e2073cf47ca4278bae8605296603d21
3
+ metadata.gz: c3f9894955745b77bd418c8cede4882f5f37bf8cd3c0e2a3eecd53baa8658e1a
4
+ data.tar.gz: d160939ff0c15d4c57b0836abe0850e9c8a499fc05eb410932dc038d2835c60f
5
5
  SHA512:
6
- metadata.gz: fbb9b4d9385dad87ba827400dc9245085c31e133d2445daf64b3d3e81f3102c189f8feafb328d085649146c8ccd2782fd7ba3cb410c262d06a31e014175cac99
7
- data.tar.gz: 55c8010cbc73d112262d674d131b1fe0acbd39b5b482e73b9b7165aac23a25a93d5bbca4320a56b97450f68af8526045aede39c13cfb3426189073acba8f6a3f
6
+ metadata.gz: 89dc3d801b5f97d2147f10bcfb691279395590b45e9631103c73f66e487345e8608e185c07f5e54429bf43d29bd3f35c017bd93cd9354a88e29d0456d1d5040f
7
+ data.tar.gz: 9893296f2801ded4b1e227a4672b0e474735d2c6ba4ff7f5e95d0591b753659a97066f422ca0615d8abbd4c1e5f9bd4c8250ff9c3c697fcb18640b3ff47dd219
@@ -690,6 +690,7 @@ module Switchman
690
690
  Switchman.cache.delete(['shard', id].join('/'))
691
691
  Switchman.cache.delete('default_shard') if default?
692
692
  end
693
+ self.class.clear_cache
693
694
  end
694
695
 
695
696
  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
@@ -85,7 +85,7 @@ module Switchman
85
85
  # Copypasta from Activerecord but with added global_id_for goodness.
86
86
  def records_for(ids)
87
87
  scope.where(association_key_name => ids).load do |record|
88
- global_key = if record.class.connection_classes == UnshardedRecord
88
+ global_key = if model.connection_classes == UnshardedRecord
89
89
  convert_key(record[association_key_name])
90
90
  else
91
91
  Shard.global_id_for(record[association_key_name], record.shard)
@@ -141,6 +141,54 @@ module Switchman
141
141
  self.class.define_attribute_methods
142
142
  super
143
143
  end
144
+
145
+ # these are called if the specific methods haven't been defined yet
146
+ def attribute(attr_name)
147
+ return super unless self.class.sharded_column?(attr_name)
148
+
149
+ reflection = self.class.send(:reflection_for_integer_attribute, attr_name)
150
+ ::Switchman::Shard.relative_id_for(super, shard, ::Switchman::Shard.current(connection_classes_for_reflection(reflection)))
151
+ end
152
+
153
+ def attribute=(attr_name, new_value)
154
+ unless self.class.sharded_column?(attr_name)
155
+ super
156
+ return
157
+ end
158
+
159
+ reflection = self.class.send(:reflection_for_integer_attribute, attr_name)
160
+ super(::Switchman::Shard.relative_id_for(new_value, ::Switchman::Shard.current(connection_classes_for_reflection(reflection)), shard))
161
+ end
162
+
163
+ def global_attribute(attr_name)
164
+ if self.class.sharded_column?(attr_name)
165
+ ::Switchman::Shard.global_id_for(attribute(attr_name), shard)
166
+ else
167
+ attribute(attr_name)
168
+ end
169
+ end
170
+
171
+ def local_attribute(attr_name)
172
+ if self.class.sharded_column?(attr_name)
173
+ ::Switchman::Shard.local_id_for(attribute(attr_name), shard).first
174
+ else
175
+ attribute(attr_name)
176
+ end
177
+ end
178
+
179
+ private
180
+
181
+ def connection_classes_for_reflection(reflection)
182
+ if reflection
183
+ if reflection.options[:polymorphic]
184
+ read_attribute(reflection.foreign_type)&.constantize&.connection_classes
185
+ else
186
+ reflection.klass.connection_classes
187
+ end
188
+ else
189
+ self.class.connection_classes
190
+ end
191
+ end
144
192
  end
145
193
  end
146
194
  end
@@ -50,7 +50,7 @@ module Switchman
50
50
 
51
51
  def calculate_simple_average(column_name, distinct)
52
52
  # See activerecord#execute_simple_calculation
53
- relation = reorder(nil)
53
+ relation = except(:order)
54
54
  column = aggregate_column(column_name)
55
55
  relation.select_values = [operation_over_aggregate_column(column, 'average', distinct).as('average'),
56
56
  operation_over_aggregate_column(column, 'count', distinct).as('count')]
@@ -20,10 +20,22 @@ module Switchman
20
20
  end
21
21
 
22
22
  module Migrator
23
+ # significant change: hash shard name, not database name
23
24
  def generate_migrator_advisory_lock_id
24
- shard_name_hash = Zlib.crc32("#{Shard.current.id}:#{Shard.current.name}")
25
+ shard_name_hash = Zlib.crc32(Shard.current.name)
25
26
  ::ActiveRecord::Migrator::MIGRATOR_SALT * shard_name_hash
26
27
  end
28
+
29
+ # significant change: strip out prefer_secondary from config
30
+ def with_advisory_lock_connection
31
+ pool = ::ActiveRecord::ConnectionAdapters::ConnectionHandler.new.establish_connection(
32
+ ::ActiveRecord::Base.connection_db_config.configuration_hash.except(:prefer_secondary)
33
+ )
34
+
35
+ pool.with_connection { |connection| yield(connection) } # rubocop:disable Style/ExplicitBlockArgument
36
+ ensure
37
+ pool&.disconnect!
38
+ end
27
39
  end
28
40
 
29
41
  module MigrationContext
@@ -40,89 +40,36 @@ 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
  name.instance_variable_set(:@schema, shard.name) if string && !name.schema
54
46
  [name.schema, name.identifier]
55
47
  end
56
48
 
57
- def view_exists?(name)
58
- name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(name.to_s)
59
- return false unless name.identifier
60
-
61
- name.instance_variable_set(:@schema, shard.name) unless name.schema
62
-
63
- select_values(<<-SQL, 'SCHEMA').any?
64
- SELECT c.relname
65
- FROM pg_class c
66
- LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
67
- WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view
68
- AND c.relname = '#{name.identifier}'
69
- AND n.nspname = '#{shard.name}'
70
- SQL
71
- end
72
-
73
- def indexes(table_name)
74
- result = query(<<-SQL, 'SCHEMA')
75
- SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid
76
- FROM pg_class t
77
- INNER JOIN pg_index d ON t.oid = d.indrelid
78
- INNER JOIN pg_class i ON d.indexrelid = i.oid
79
- WHERE i.relkind = 'i'
80
- AND d.indisprimary = 'f'
81
- AND t.relname = '#{table_name}'
82
- AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = '#{shard.name}' )
83
- ORDER BY i.relname
84
- SQL
85
-
86
- result.map do |row|
87
- index_name = row[0]
88
- unique = row[1] == true || row[1] == 't'
89
- indkey = row[2].split
90
- inddef = row[3]
91
- oid = row[4]
92
-
93
- columns = Hash[query(<<-SQL, 'SCHEMA')] # rubocop:disable Style/HashConversion
94
- SELECT a.attnum, a.attname
95
- FROM pg_attribute a
96
- WHERE a.attrelid = #{oid}
97
- AND a.attnum IN (#{indkey.join(',')})
98
- SQL
99
-
100
- column_names = columns.stringify_keys.values_at(*indkey).compact
101
-
102
- next if column_names.empty?
103
-
104
- # add info on sort order for columns (only desc order is explicitly specified, asc is the default)
105
- desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
106
- orders = desc_order_columns.any? ? Hash[desc_order_columns.map { |order_column| [order_column, :desc] }] : {} # rubocop:disable Style/HashConversion
107
- where = inddef.scan(/WHERE (.+)$/).flatten[0]
108
- using = inddef.scan(/USING (.+?) /).flatten[0].to_sym
109
-
110
- ::ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, index_name, unique, column_names,
111
- orders: orders, where: where, using: using)
112
- end.compact
49
+ # significant change: use the shard name if no explicit schema
50
+ def quoted_scope(name = nil, type: nil)
51
+ schema, name = extract_schema_qualified_name(name)
52
+ type = \
53
+ case type # rubocop:disable Style/HashLikeCase
54
+ when 'BASE TABLE'
55
+ "'r','p'"
56
+ when 'VIEW'
57
+ "'v','m'"
58
+ when 'FOREIGN TABLE'
59
+ "'f'"
60
+ end
61
+ scope = {}
62
+ scope[:schema] = quote(schema || shard.name)
63
+ scope[:name] = quote(name) if name
64
+ scope[:type] = type if type
65
+ scope
113
66
  end
114
67
 
115
- def index_name_exists?(table_name, index_name, _default = nil)
116
- exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i.positive?
117
- SELECT COUNT(*)
118
- FROM pg_class t
119
- INNER JOIN pg_index d ON t.oid = d.indrelid
120
- INNER JOIN pg_class i ON d.indexrelid = i.oid
121
- WHERE i.relkind = 'i'
122
- AND i.relname = '#{index_name}'
123
- AND t.relname = '#{table_name}'
124
- AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = '#{shard.name}' )
125
- SQL
68
+ def foreign_keys(table_name)
69
+ super.each do |fk|
70
+ to_table_qualified_name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(fk.to_table)
71
+ fk.to_table = to_table_qualified_name.identifier if to_table_qualified_name.schema == shard.name
72
+ end
126
73
  end
127
74
 
128
75
  def quote_local_table_name(name)
@@ -140,6 +87,10 @@ module Switchman
140
87
  name.quoted
141
88
  end
142
89
 
90
+ def with_global_table_name(&block)
91
+ with_local_table_name(false, &block)
92
+ end
93
+
143
94
  def with_local_table_name(enable = true) # rubocop:disable Style/OptionalBooleanParameter
144
95
  old_value = @use_local_table_name
145
96
  @use_local_table_name = enable
@@ -148,41 +99,6 @@ module Switchman
148
99
  @use_local_table_name = old_value
149
100
  end
150
101
 
151
- def foreign_keys(table_name)
152
- # mostly copy-pasted from AR - only change is to the nspname condition for qualified names support
153
- fk_info = select_all <<-SQL.strip_heredoc
154
- 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
155
- FROM pg_constraint c
156
- JOIN pg_class t1 ON c.conrelid = t1.oid
157
- JOIN pg_class t2 ON c.confrelid = t2.oid
158
- JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
159
- JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
160
- JOIN pg_namespace t3 ON c.connamespace = t3.oid
161
- WHERE c.contype = 'f'
162
- AND t1.relname = #{quote(table_name)}
163
- AND t3.nspname = '#{shard.name}'
164
- ORDER BY c.conname
165
- SQL
166
-
167
- fk_info.map do |row|
168
- options = {
169
- column: row['column'],
170
- name: row['name'],
171
- primary_key: row['primary_key']
172
- }
173
-
174
- options[:on_delete] = extract_foreign_key_action(row['on_delete'])
175
- options[:on_update] = extract_foreign_key_action(row['on_update'])
176
-
177
- # strip the schema name from to_table if it matches
178
- to_table = row['to_table']
179
- to_table_qualified_name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(to_table)
180
- to_table = to_table_qualified_name.identifier if to_table_qualified_name.schema == shard.name
181
-
182
- ::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(table_name, to_table, options)
183
- end
184
- end
185
-
186
102
  def add_index_options(_table_name, _column_name, **)
187
103
  index, algorithm, if_not_exists = super
188
104
  algorithm = nil if DatabaseServer.creating_new_shard && algorithm == 'CONCURRENTLY'
@@ -15,7 +15,7 @@ module Switchman
15
15
  def convert_to_id(value)
16
16
  case value
17
17
  when ::ActiveRecord::Base
18
- value.send(primary_key)
18
+ value.id
19
19
  else
20
20
  super
21
21
  end
@@ -239,6 +239,10 @@ module Switchman
239
239
  connection.with_local_table_name { super }
240
240
  end
241
241
 
242
+ def table_name_matches?(from)
243
+ connection.with_global_table_name { super }
244
+ end
245
+
242
246
  def transpose_predicates(predicates,
243
247
  source_shard,
244
248
  target_shard,
@@ -281,7 +285,7 @@ module Switchman
281
285
  remove = true if type == :primary &&
282
286
  remove_nonlocal_primary_keys &&
283
287
  predicate.left.relation.klass == klass &&
284
- predicate.is_a?(::Arel::Nodes::Equality)
288
+ (predicate.is_a?(::Arel::Nodes::Equality) || predicate.is_a?(::Arel::Nodes::HomogeneousIn))
285
289
 
286
290
  current_source_shard =
287
291
  if source_shard
@@ -301,7 +305,7 @@ module Switchman
301
305
  new_right_value =
302
306
  case right
303
307
  when Array
304
- right.map { |val| transpose_predicate_value(val, current_source_shard, target_shard, type, remove) }
308
+ right.map { |val| transpose_predicate_value(val, current_source_shard, target_shard, type, remove).presence }.compact
305
309
  else
306
310
  transpose_predicate_value(right, current_source_shard, target_shard, type, remove)
307
311
  end
@@ -315,7 +319,13 @@ module Switchman
315
319
  predicate.class.new(predicate.left, right.class.new(new_right_value, right.attribute))
316
320
  end
317
321
  elsif predicate.is_a?(::Arel::Nodes::HomogeneousIn)
318
- predicate.class.new(new_right_value, predicate.attribute, predicate.type)
322
+ # switch to a regular In, so that Relation::WhereClause#contradiction? knows about it
323
+ if new_right_value.empty?
324
+ klass = predicate.type == :in ? ::Arel::Nodes::In : ::Arel::Nodes::NotIn
325
+ klass.new(predicate.attribute, new_right_value)
326
+ else
327
+ predicate.class.new(new_right_value, predicate.attribute, predicate.type)
328
+ end
319
329
  else
320
330
  predicate.class.new(predicate.left, new_right_value)
321
331
  end
@@ -63,7 +63,7 @@ module Switchman
63
63
  %I[update_all delete_all].each do |method|
64
64
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
65
65
  def #{method}(*args)
66
- result = self.activate { |relation| relation.call_super(#{method.inspect}, Relation, *args) }
66
+ result = self.activate(unordered: true) { |relation| relation.call_super(#{method.inspect}, Relation, *args) }
67
67
  result = result.sum if result.is_a?(Array)
68
68
  result
69
69
  end
@@ -103,7 +103,7 @@ module Switchman
103
103
  end
104
104
  end
105
105
 
106
- def activate(&block)
106
+ def activate(unordered: false, &block)
107
107
  shards = all_shards
108
108
  if Array === shards && shards.length == 1
109
109
  if shards.first == DefaultShard || shards.first == Shard.current(klass.connection_classes)
@@ -112,10 +112,61 @@ module Switchman
112
112
  shards.first.activate(klass.connection_classes) { yield(self, shards.first) }
113
113
  end
114
114
  else
115
- # TODO: implement local limit to avoid querying extra shards
116
- Shard.with_each_shard(shards, [klass.connection_classes]) do
117
- shard(Shard.current(klass.connection_classes), :to_a).activate(&block)
115
+ result_count = 0
116
+ can_order = false
117
+ result = Shard.with_each_shard(shards, [klass.connection_classes]) do
118
+ # don't even query other shards if we're already past the limit
119
+ next if limit_value && result_count >= limit_value && order_values.empty?
120
+
121
+ relation = shard(Shard.current(klass.connection_classes), :to_a)
122
+ # do a minimal query if possible
123
+ relation = relation.limit(limit_value - result_count) if limit_value && !result_count.zero? && order_values.empty?
124
+
125
+ shard_results = relation.activate(&block)
126
+
127
+ if shard_results.present? && !unordered
128
+ can_order ||= can_order_cross_shard_results? unless order_values.empty?
129
+ raise OrderOnMultiShardQuery if !can_order && !order_values.empty? && result_count.positive?
130
+
131
+ result_count += shard_results.is_a?(Array) ? shard_results.length : 1
132
+ end
133
+ shard_results
134
+ end
135
+
136
+ result = reorder_cross_shard_results(result) if can_order
137
+ result.slice!(limit_value..-1) if limit_value
138
+ result
139
+ end
140
+ end
141
+
142
+ def can_order_cross_shard_results?
143
+ # we only presume to be able to post-sort the most basic of orderings
144
+ order_values.all? { |ov| ov.is_a?(::Arel::Nodes::Ordering) && ov.expr.is_a?(::Arel::Attributes::Attribute) }
145
+ end
146
+
147
+ def reorder_cross_shard_results(results)
148
+ results.sort! do |l, r|
149
+ result = 0
150
+ order_values.each do |ov|
151
+ if l.is_a?(::ActiveRecord::Base)
152
+ a = l.attribute(ov.expr.name)
153
+ b = r.attribute(ov.expr.name)
154
+ else
155
+ a, b = l, r
156
+ end
157
+ next if a == b
158
+
159
+ if a.nil? || b.nil?
160
+ result = 1 if a.nil?
161
+ result *= -1 if ov.is_a?(::Arel::Nodes::Descending)
162
+ else
163
+ result = a <=> b
164
+ end
165
+
166
+ result *= -1 if ov.is_a?(::Arel::Nodes::Descending)
167
+ break unless result.zero?
118
168
  end
169
+ result
119
170
  end
120
171
  end
121
172
  end
@@ -113,7 +113,7 @@ module Switchman
113
113
  end
114
114
 
115
115
  def config(environment = :primary)
116
- @configs[environment] ||= begin
116
+ @configs[environment] ||=
117
117
  case @config[environment]
118
118
  when Array
119
119
  @config[environment].map do |config|
@@ -127,7 +127,6 @@ module Switchman
127
127
  else
128
128
  @config
129
129
  end
130
- end
131
130
  end
132
131
 
133
132
  def guard_rail_environment
@@ -214,7 +213,7 @@ module Switchman
214
213
  ::ActiveRecord::Migration.verbose = false
215
214
 
216
215
  unless schema == false
217
- shard.activate do
216
+ shard.activate(*Shard.sharded_models) do
218
217
  reset_column_information
219
218
 
220
219
  ::ActiveRecord::Base.connection.transaction(requires_new: true) do
@@ -150,6 +150,7 @@ module Switchman
150
150
  ::ActiveRecord::Relation.include(CallSuper)
151
151
 
152
152
  ::ActiveRecord::PredicateBuilder::AssociationQueryValue.prepend(ActiveRecord::PredicateBuilder::AssociationQueryValue)
153
+ ::ActiveRecord::PredicateBuilder::PolymorphicArrayValue.prepend(ActiveRecord::PredicateBuilder::AssociationQueryValue)
153
154
 
154
155
  ::ActiveRecord::Tasks::DatabaseTasks.singleton_class.prepend(ActiveRecord::Tasks::DatabaseTasks)
155
156
 
@@ -19,7 +19,7 @@ module Switchman
19
19
  end
20
20
 
21
21
  server1 = Shard.default.database_server
22
- server2 = DatabaseServer.create(Shard.default.database_server.config)
22
+ server2 = DatabaseServer.create(Shard.default.database_server.config.merge(server2: true))
23
23
 
24
24
  if server1 == Shard.default.database_server && server1.config[:shard1] && server1.config[:shard2]
25
25
  # 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 = '3.0.0'
4
+ VERSION = '3.0.5'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: switchman
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody Cutrer
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2021-03-17 00:00:00.000000000 Z
13
+ date: 2021-06-11 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -289,7 +289,6 @@ files:
289
289
  - lib/switchman/open4.rb
290
290
  - lib/switchman/r_spec_helper.rb
291
291
  - lib/switchman/rails.rb
292
- - lib/switchman/schema_cache.rb
293
292
  - lib/switchman/sharded_instrumenter.rb
294
293
  - lib/switchman/standard_error.rb
295
294
  - lib/switchman/test_helper.rb
@@ -314,7 +313,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
314
313
  - !ruby/object:Gem::Version
315
314
  version: '0'
316
315
  requirements: []
317
- rubygems_version: 3.1.4
316
+ rubygems_version: 3.2.15
318
317
  signing_key:
319
318
  specification_version: 4
320
319
  summary: Rails sharding magic
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Switchman
4
- class SchemaCache < ::ActiveRecord::ConnectionAdapters::SchemaCache
5
- delegate :connection, to: :pool
6
- attr_reader :pool
7
-
8
- def initialize(pool)
9
- @pool = pool
10
- super(nil)
11
- end
12
-
13
- def copy_values(other_cache)
14
- # use the same cached values but still fall back to the correct pool
15
- %i[@columns @columns_hash @primary_keys @data_sources].each do |iv|
16
- instance_variable_set(iv, other_cache.instance_variable_get(iv))
17
- end
18
- end
19
- end
20
- end