switchman 3.0.0 → 3.0.5

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