switchman 3.0.24 → 4.2.4

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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +16 -15
  3. data/db/migrate/20180828183945_add_default_shard_index.rb +1 -1
  4. data/db/migrate/20190114212900_add_unique_name_indexes.rb +10 -4
  5. data/lib/switchman/active_record/abstract_adapter.rb +11 -7
  6. data/lib/switchman/active_record/associations.rb +157 -50
  7. data/lib/switchman/active_record/attribute_methods.rb +192 -101
  8. data/lib/switchman/active_record/base.rb +136 -33
  9. data/lib/switchman/active_record/calculations.rb +91 -48
  10. data/lib/switchman/active_record/connection_handler.rb +18 -0
  11. data/lib/switchman/active_record/connection_pool.rb +41 -6
  12. data/lib/switchman/active_record/database_configurations.rb +23 -13
  13. data/lib/switchman/active_record/finder_methods.rb +22 -16
  14. data/lib/switchman/active_record/log_subscriber.rb +3 -6
  15. data/lib/switchman/active_record/migration.rb +42 -17
  16. data/lib/switchman/active_record/model_schema.rb +1 -1
  17. data/lib/switchman/active_record/pending_migration_connection.rb +17 -0
  18. data/lib/switchman/active_record/persistence.rb +32 -2
  19. data/lib/switchman/active_record/postgresql_adapter.rb +37 -22
  20. data/lib/switchman/active_record/predicate_builder.rb +2 -2
  21. data/lib/switchman/active_record/query_cache.rb +26 -17
  22. data/lib/switchman/active_record/query_methods.rb +249 -142
  23. data/lib/switchman/active_record/reflection.rb +10 -3
  24. data/lib/switchman/active_record/relation.rb +103 -32
  25. data/lib/switchman/active_record/spawn_methods.rb +3 -7
  26. data/lib/switchman/active_record/statement_cache.rb +13 -9
  27. data/lib/switchman/active_record/table_definition.rb +1 -1
  28. data/lib/switchman/active_record/tasks/database_tasks.rb +6 -1
  29. data/lib/switchman/active_record/test_fixtures.rb +71 -25
  30. data/lib/switchman/active_support/cache.rb +9 -4
  31. data/lib/switchman/arel.rb +16 -25
  32. data/lib/switchman/call_super.rb +2 -2
  33. data/lib/switchman/database_server.rb +68 -34
  34. data/lib/switchman/default_shard.rb +14 -3
  35. data/lib/switchman/engine.rb +36 -19
  36. data/lib/switchman/environment.rb +2 -2
  37. data/lib/switchman/errors.rb +13 -0
  38. data/lib/switchman/guard_rail/relation.rb +3 -3
  39. data/lib/switchman/parallel.rb +6 -6
  40. data/lib/switchman/r_spec_helper.rb +12 -11
  41. data/lib/switchman/shard.rb +182 -72
  42. data/lib/switchman/sharded_instrumenter.rb +9 -3
  43. data/lib/switchman/shared_schema_cache.rb +11 -0
  44. data/lib/switchman/standard_error.rb +4 -0
  45. data/lib/switchman/test_helper.rb +3 -3
  46. data/lib/switchman/version.rb +1 -1
  47. data/lib/switchman.rb +27 -15
  48. data/lib/tasks/switchman.rake +96 -60
  49. metadata +18 -168
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 469ef6bca62776e9ba974a5c6d2a6220fc6ddcf1649956f43247465cbfb24f37
4
- data.tar.gz: 9fefef5095835cf28a6c72197ed74f6de9974d7c9c9019ad4ea87209d98c37e9
3
+ metadata.gz: b26ae7fe0a43054e224f7104377e1a47d8cb56e4a5e05cb443f76045901f8298
4
+ data.tar.gz: 220c7665740e8ff3cf7a27b0bed847e744bd2d0732c35d93ecd05489b71ccd7c
5
5
  SHA512:
6
- metadata.gz: 96d01726ff4e0e560e63964c5607417b7272202109b6e7e718769e9ba2cc04f82000f442c70aef8d6140e6dac00925197f0a95c70b790d47fbc2355d9021ae39
7
- data.tar.gz: 4b2edb3c0400f3280ba579bfaac61fce263ea5dd0881e5eeb77e675e0d2c1dfe04168e059f7bb91ac601ce2ef210c50c252945258fe2166ee6cf21c458defac0
6
+ metadata.gz: 2dc2d565571d81b9e222c3b083095a6b11ff2c22b4f756ed0e2e03d859e44d1d3f3e1696a3510ee5fa3e97b4595a2a5def19e70f0a5bd28d547fc6c277ae4631
7
+ data.tar.gz: 9780190480638be3fedc7b15bd6c34a5e3de8478051e18835684aec755752a894236145f9ba5061fafc483fa3b500d2da51ce4fb95748a809d3c506620cb8167
data/Rakefile CHANGED
@@ -2,37 +2,38 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  begin
5
- require 'bundler/setup'
5
+ require "bundler/setup"
6
6
  rescue LoadError
7
- puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
7
+ puts "You must `gem install bundler` and `bundle install` to run rake tasks"
8
8
  end
9
9
  begin
10
- require 'rdoc/task'
10
+ require "rdoc/task"
11
11
  rescue LoadError
12
- require 'rdoc/rdoc'
13
- require 'rake/rdoctask'
12
+ require "rdoc/rdoc"
13
+ require "rake/rdoctask"
14
14
  RDoc::Task = Rake::RDocTask
15
15
  end
16
16
 
17
17
  RDoc::Task.new(:rdoc) do |rdoc|
18
- rdoc.rdoc_dir = 'rdoc'
19
- rdoc.title = 'Switchman'
20
- rdoc.options << '--line-numbers'
21
- rdoc.rdoc_files.include('lib/**/*.rb')
18
+ rdoc.rdoc_dir = "rdoc"
19
+ rdoc.title = "Switchman"
20
+ rdoc.options << "--line-numbers"
21
+ rdoc.rdoc_files.include("lib/**/*.rb")
22
22
  end
23
23
 
24
- APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
25
- load 'rails/tasks/engine.rake'
24
+ load "./spec/tasks/coverage.rake"
25
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
26
+ load "rails/tasks/engine.rake"
26
27
 
27
28
  Bundler::GemHelper.install_tasks
28
29
 
29
- require 'rspec/core/rake_task'
30
+ require "rspec/core/rake_task"
30
31
  RSpec::Core::RakeTask.new
31
32
 
32
- require 'rubocop/rake_task'
33
+ require "rubocop/rake_task"
33
34
 
34
35
  RuboCop::RakeTask.new do |task|
35
- task.options = ['-S']
36
+ task.options = ["-S"]
36
37
  end
37
38
 
38
- task default: %i[spec rubocop]
39
+ task default: %i[spec]
@@ -5,7 +5,7 @@ class AddDefaultShardIndex < ActiveRecord::Migration[4.2]
5
5
  Switchman::Shard.where(default: nil).update_all(default: false) if Switchman::Shard.current.default?
6
6
  change_column_default :switchman_shards, :default, false
7
7
  change_column_null :switchman_shards, :default, false
8
- options = if connection.adapter_name == 'PostgreSQL'
8
+ options = if connection.adapter_name == "PostgreSQL"
9
9
  { unique: true, where: '"default"' }
10
10
  else
11
11
  {}
@@ -3,9 +3,15 @@
3
3
  class AddUniqueNameIndexes < ActiveRecord::Migration[4.2]
4
4
  def change
5
5
  add_index :switchman_shards, %i[database_server_id name], unique: true
6
- add_index :switchman_shards, :database_server_id, unique: true, where: 'name IS NULL',
7
- name: 'index_switchman_shards_unique_primary_shard'
8
- add_index :switchman_shards, '(true)', unique: true, where: 'database_server_id IS NULL AND name IS NULL',
9
- name: 'index_switchman_shards_unique_primary_db_and_shard'
6
+ add_index :switchman_shards,
7
+ :database_server_id,
8
+ unique: true,
9
+ where: "name IS NULL",
10
+ name: "index_switchman_shards_unique_primary_shard"
11
+ add_index :switchman_shards,
12
+ "(true)",
13
+ unique: true,
14
+ where: "database_server_id IS NULL AND name IS NULL",
15
+ name: "index_switchman_shards_unique_primary_db_and_shard"
10
16
  end
11
17
  end
@@ -5,7 +5,7 @@ module Switchman
5
5
  module AbstractAdapter
6
6
  module ForeignKeyCheck
7
7
  def add_column(table, name, type, limit: nil, **)
8
- Switchman.foreign_key_check(name, type, limit: limit)
8
+ Switchman.foreign_key_check(name, type, limit:)
9
9
  super
10
10
  end
11
11
  end
@@ -19,21 +19,25 @@ module Switchman
19
19
 
20
20
  def initialize(*args)
21
21
  super
22
- @instrumenter = Switchman::ShardedInstrumenter.new(@instrumenter, self)
22
+
23
+ @instrumenter = Switchman::ShardedInstrumenter.new(@instrumenter, self) if ::Rails.version < "8.0"
24
+
23
25
  @last_query_at = Time.now
24
26
  end
25
27
 
26
- def quote_local_table_name(name)
27
- quote_table_name(name)
28
+ if ::Rails.version >= "8.0"
29
+ def instrumenter # :nodoc:
30
+ @instrumenter ||= Switchman::ShardedInstrumenter.new(::ActiveSupport::Notifications.instrumenter, self)
31
+ end
28
32
  end
29
33
 
30
- def schema_migration
31
- ::ActiveRecord::SchemaMigration
34
+ def quote_local_table_name(name)
35
+ quote_table_name(name)
32
36
  end
33
37
 
34
38
  protected
35
39
 
36
- def log(*args, &block)
40
+ def log(...)
37
41
  super
38
42
  ensure
39
43
  @last_query_at = Time.now
@@ -23,12 +23,26 @@ module Switchman
23
23
  end
24
24
 
25
25
  module CollectionAssociation
26
- def find_target
27
- shards = reflection.options[:multishard] && owner.respond_to?(:associated_shards) ? owner.associated_shards : [shard]
26
+ def find_target(async: false)
27
+ shards = if reflection.options[:multishard] && owner.respond_to?(:associated_shards)
28
+ owner.associated_shards
29
+ else
30
+ [shard]
31
+ end
28
32
  # activate both the owner and the target's shard category, so that Reflection#join_id_for,
29
33
  # when called for the owner, will be returned relative to shard the query will execute on
30
- Shard.with_each_shard(shards, [klass.connection_classes, owner.class.connection_classes].uniq) do
31
- super
34
+ Shard.with_each_shard(shards,
35
+ [klass.connection_class_for_self, owner.class.connection_class_for_self].uniq) do
36
+ if reflection.options[:multishard] && owner.respond_to?(:associated_shards) && reflection.has_scope?
37
+ # Prevent duplicate results when reflection has a scope (when it would use the skip_statement_cache? path)
38
+ # otherwise, the super call will set the shard_value to the object, causing it to iterate too many times
39
+ # over the associated shards
40
+ scope.shard(Shard.current(scope.klass.connection_class_for_self), :association).to_a
41
+ elsif ::Rails.version < "8.0"
42
+ super()
43
+ else
44
+ super
45
+ end
32
46
  end
33
47
  end
34
48
 
@@ -50,7 +64,7 @@ module Switchman
50
64
  def shard
51
65
  if @owner.class.sharded_column?(@reflection.foreign_key) &&
52
66
  (foreign_id = @owner[@reflection.foreign_key])
53
- Shard.shard_for(foreign_id, @owner.shard)
67
+ Shard.shard_for(foreign_id, @owner.loaded_from_shard)
54
68
  else
55
69
  super
56
70
  end
@@ -83,10 +97,74 @@ module Switchman
83
97
 
84
98
  module Preloader
85
99
  module Association
100
+ # significant changes:
101
+ # * associate shards with records
102
+ # * look on all appropriate shards when loading records
103
+ module LoaderRecords
104
+ def populate_keys_to_load_and_already_loaded_records
105
+ @sharded_keys_to_load = {}
106
+
107
+ loaders.each do |loader|
108
+ multishard = loader.send(:reflection).options[:multishard]
109
+ belongs_to = loader.send(:reflection).macro == :belongs_to
110
+ loader.owners_by_key.each do |key, owners|
111
+ if (loaded_owner = owners.find { |owner| loader.loaded?(owner) })
112
+ already_loaded_records_by_key[key] = loader.target_for(loaded_owner)
113
+ else
114
+ shard_set = @sharded_keys_to_load[key] ||= Set.new
115
+ owner_key_name = loader.send(:owner_key_name)
116
+ owners.each do |owner|
117
+ if multishard && owner.respond_to?(:associated_shards)
118
+ shard_set.merge(owner.associated_shards.map(&:id))
119
+ elsif belongs_to && owner.class.sharded_column?(owner_key_name)
120
+ shard_set.add(Shard.shard_for(owner[owner_key_name], owner.shard).id)
121
+ elsif belongs_to
122
+ shard_set.add(Shard.current.id)
123
+ else
124
+ shard_set.add(owner.shard.id)
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ @sharded_keys_to_load.delete_if { |key, _shards| already_loaded_records_by_key.include?(key) }
132
+ end
133
+
134
+ def load_records
135
+ ret = []
136
+
137
+ shards_with_keys = @sharded_keys_to_load.each_with_object({}) do |(key, shards), h|
138
+ shards.each { |shard| (h[shard] ||= []) << key }
139
+ end
140
+
141
+ shards_with_keys.each do |shard, keys|
142
+ Shard.lookup(shard).activate do
143
+ scope_was = loader_query.scope
144
+ begin
145
+ loader_query.instance_variable_set(
146
+ :@scope,
147
+ loader_query.scope.shard(
148
+ Shard.current(loader_query.scope.model.connection_class_for_self)
149
+ )
150
+ )
151
+ ret += loader_query.load_records_for_keys(keys) do |record|
152
+ loaders.each { |l| l.set_inverse(record) }
153
+ end
154
+ ensure
155
+ loader_query.instance_variable_set(:@scope, scope_was)
156
+ end
157
+ end
158
+ end
159
+
160
+ ret
161
+ end
162
+ end
163
+
86
164
  # Copypasta from Activerecord but with added global_id_for goodness.
87
165
  def records_for(ids)
88
166
  scope.where(association_key_name => ids).load do |record|
89
- global_key = if model.connection_classes == UnshardedRecord
167
+ global_key = if model.connection_class_for_self == UnshardedRecord
90
168
  convert_key(record[association_key_name])
91
169
  else
92
170
  Shard.global_id_for(record[association_key_name], record.shard)
@@ -97,46 +175,37 @@ module Switchman
97
175
  end
98
176
  end
99
177
 
178
+ # Disabling to keep closer to rails original
179
+ # rubocop:disable Naming/AccessorMethodName
180
+ # significant changes:
181
+ # * globalize the key to lookup
182
+ def set_inverse(record)
183
+ global_key = if model.connection_class_for_self == UnshardedRecord
184
+ convert_key(record[association_key_name])
185
+ else
186
+ Shard.global_id_for(record[association_key_name], record.shard)
187
+ end
188
+
189
+ if (owners = owners_by_key[convert_key(global_key)])
190
+ # Processing only the first owner
191
+ # because the record is modified but not an owner
192
+ association = owners.first.association(reflection.name)
193
+ association.set_inverse_instance(record)
194
+ end
195
+ end
196
+ # rubocop:enable Naming/AccessorMethodName
197
+
100
198
  # significant changes:
101
199
  # * partition_by_shard the records_for call
102
200
  # * re-globalize the fetched owner id before looking up in the map
103
- def load_records
201
+ # TODO: the ignored param currently loads records; we should probably not waste effort double-loading them
202
+ # Change introduced here: https://github.com/rails/rails/commit/c6c0b2e8af64509b699b782aadfecaa430700ece
203
+ def load_records(raw_records = nil)
104
204
  # owners can be duplicated when a relation has a collection association join
105
205
  # #compare_by_identity makes such owners different hash keys
106
206
  @records_by_owner = {}.compare_by_identity
107
207
 
108
- if owner_keys.empty?
109
- raw_records = []
110
- else
111
- # determine the shard to search for each owner
112
- if reflection.macro == :belongs_to
113
- # for belongs_to, it's the shard of the foreign_key
114
- partition_proc = lambda do |owner|
115
- if owner.class.sharded_column?(owner_key_name)
116
- Shard.shard_for(owner[owner_key_name], owner.shard)
117
- else
118
- Shard.current
119
- end
120
- end
121
- elsif !reflection.options[:multishard]
122
- # for non-multishard associations, it's *just* the owner's shard
123
- partition_proc = ->(owner) { owner.shard }
124
- end
125
-
126
- raw_records = Shard.partition_by_shard(owners, partition_proc) do |partitioned_owners|
127
- relative_owner_keys = partitioned_owners.map do |owner|
128
- key = owner[owner_key_name]
129
- if key && owner.class.sharded_column?(owner_key_name)
130
- key = Shard.relative_id_for(key, owner.shard,
131
- Shard.current(klass.connection_classes))
132
- end
133
- convert_key(key)
134
- end
135
- relative_owner_keys.compact!
136
- relative_owner_keys.uniq!
137
- records_for(relative_owner_keys)
138
- end
139
- end
208
+ raw_records ||= loader_query.records_for([self])
140
209
 
141
210
  @preloaded_records = raw_records.select do |record|
142
211
  assignments = false
@@ -147,7 +216,7 @@ module Switchman
147
216
  record.shard)
148
217
  end
149
218
 
150
- owners_by_key[convert_key(owner_key)].each do |owner|
219
+ owners_by_key[convert_key(owner_key)]&.each do |owner|
151
220
  entries = (@records_by_owner[owner] ||= [])
152
221
 
153
222
  if reflection.collection? || entries.empty?
@@ -184,23 +253,61 @@ module Switchman
184
253
  self.shard_source_value = :association
185
254
  end
186
255
 
187
- def shard(*args)
188
- scope.shard(*args)
256
+ def shard(*)
257
+ scope.shard(*)
189
258
  end
190
259
  end
191
260
 
192
261
  module AutosaveAssociation
193
- def record_changed?(reflection, record, key)
194
- record.new_record? ||
195
- (record.has_attribute?(reflection.foreign_key) && record.send(reflection.foreign_key) != key) || # have to use send instead of [] because sharding
196
- record.attribute_changed?(reflection.foreign_key)
262
+ def association_foreign_key_changed?(reflection, record, key)
263
+ return false if reflection.through_reflection?
264
+
265
+ foreign_key = Array(reflection.foreign_key)
266
+ return false unless foreign_key.all? { |k| record._has_attribute?(k) }
267
+
268
+ # have to use send instead of _read_attribute because sharding
269
+ foreign_key.map { |k| record.send(k) } != Array(key)
197
270
  end
198
271
 
199
272
  def save_belongs_to_association(reflection)
200
- # this seems counter-intuitive, but the autosave code will assign to attribute bypassing switchman,
201
- # after reading the id attribute _without_ bypassing switchman. So we need Shard.current for the
202
- # category of the associated record to match Shard.current for the category of self
203
- shard.activate(connection_classes_for_reflection(reflection)) { super }
273
+ association = association_instance_get(reflection.name)
274
+ return unless association&.loaded? && !association.stale_target?
275
+
276
+ record = association.load_target
277
+ return unless record && !record.destroyed?
278
+
279
+ autosave = reflection.options[:autosave]
280
+
281
+ if autosave && record.marked_for_destruction?
282
+ foreign_key = Array(reflection.foreign_key)
283
+ foreign_key.each { |key| self[key] = nil }
284
+ record.destroy
285
+ elsif autosave != false
286
+ saved = record.save(validate: !autosave) if record.new_record? || (autosave && record.changed_for_autosave?)
287
+
288
+ if association.updated?
289
+ primary_key = Array(compute_primary_key(reflection, record)).map(&:to_s)
290
+ foreign_key = Array(reflection.foreign_key)
291
+
292
+ primary_key_foreign_key_pairs = primary_key.zip(foreign_key)
293
+ primary_key_foreign_key_pairs.each do |pk, fk|
294
+ # Notable change: add relative_id_for here
295
+ association_id = if record.class.sharded_column?(pk)
296
+ Shard.relative_id_for(
297
+ record._read_attribute(pk),
298
+ record.shard,
299
+ shard
300
+ )
301
+ else
302
+ record._read_attribute(pk)
303
+ end
304
+ self[fk] = association_id unless self[fk] == association_id
305
+ end
306
+ association.loaded!
307
+ end
308
+
309
+ saved if autosave
310
+ end
204
311
  end
205
312
  end
206
313
  end