switchman 3.0.2 → 4.2.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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +16 -15
  3. data/db/migrate/20180828183945_add_default_shard_index.rb +2 -2
  4. data/db/migrate/20180828192111_add_timestamps_to_shards.rb +1 -1
  5. data/db/migrate/20190114212900_add_unique_name_indexes.rb +10 -4
  6. data/lib/switchman/action_controller/caching.rb +2 -2
  7. data/lib/switchman/active_record/abstract_adapter.rb +11 -18
  8. data/lib/switchman/active_record/associations.rb +315 -0
  9. data/lib/switchman/active_record/attribute_methods.rb +191 -79
  10. data/lib/switchman/active_record/base.rb +204 -50
  11. data/lib/switchman/active_record/calculations.rb +92 -49
  12. data/lib/switchman/active_record/connection_handler.rb +18 -0
  13. data/lib/switchman/active_record/connection_pool.rb +47 -34
  14. data/lib/switchman/active_record/database_configurations.rb +32 -6
  15. data/lib/switchman/active_record/finder_methods.rb +22 -16
  16. data/lib/switchman/active_record/log_subscriber.rb +3 -6
  17. data/lib/switchman/active_record/migration.rb +42 -14
  18. data/lib/switchman/active_record/model_schema.rb +1 -1
  19. data/lib/switchman/active_record/pending_migration_connection.rb +17 -0
  20. data/lib/switchman/active_record/persistence.rb +37 -2
  21. data/lib/switchman/active_record/postgresql_adapter.rb +39 -20
  22. data/lib/switchman/active_record/predicate_builder.rb +2 -2
  23. data/lib/switchman/active_record/query_cache.rb +26 -17
  24. data/lib/switchman/active_record/query_methods.rb +251 -140
  25. data/lib/switchman/active_record/reflection.rb +10 -3
  26. data/lib/switchman/active_record/relation.rb +110 -35
  27. data/lib/switchman/active_record/spawn_methods.rb +3 -7
  28. data/lib/switchman/active_record/statement_cache.rb +13 -9
  29. data/lib/switchman/active_record/table_definition.rb +1 -1
  30. data/lib/switchman/active_record/tasks/database_tasks.rb +6 -1
  31. data/lib/switchman/active_record/test_fixtures.rb +89 -0
  32. data/lib/switchman/active_support/cache.rb +25 -4
  33. data/lib/switchman/arel.rb +20 -7
  34. data/lib/switchman/call_super.rb +2 -2
  35. data/lib/switchman/database_server.rb +123 -83
  36. data/lib/switchman/default_shard.rb +14 -5
  37. data/lib/switchman/engine.rb +85 -131
  38. data/lib/switchman/environment.rb +2 -2
  39. data/lib/switchman/errors.rb +17 -2
  40. data/lib/switchman/guard_rail/relation.rb +7 -10
  41. data/lib/switchman/guard_rail.rb +5 -0
  42. data/lib/switchman/parallel.rb +68 -0
  43. data/lib/switchman/r_spec_helper.rb +17 -28
  44. data/lib/switchman/rails.rb +1 -4
  45. data/{app/models → lib}/switchman/shard.rb +229 -246
  46. data/lib/switchman/sharded_instrumenter.rb +9 -3
  47. data/lib/switchman/shared_schema_cache.rb +11 -0
  48. data/lib/switchman/standard_error.rb +15 -12
  49. data/lib/switchman/test_helper.rb +3 -3
  50. data/{app/models → lib}/switchman/unsharded_record.rb +1 -1
  51. data/lib/switchman/version.rb +1 -1
  52. data/lib/switchman.rb +44 -12
  53. data/lib/tasks/switchman.rake +101 -54
  54. metadata +34 -176
  55. data/lib/switchman/active_record/association.rb +0 -206
  56. data/lib/switchman/open4.rb +0 -80
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'securerandom'
3
+ require "securerandom"
4
4
 
5
5
  module Switchman
6
6
  class DatabaseServer
@@ -8,24 +8,25 @@ module Switchman
8
8
 
9
9
  class << self
10
10
  attr_accessor :creating_new_shard
11
+ attr_reader :all_roles
12
+
13
+ include Enumerable
14
+
15
+ delegate :each, to: :all
11
16
 
12
17
  def all
13
18
  database_servers.values
14
19
  end
15
20
 
16
- def all_roles
17
- @all_roles ||= all.map(&:roles).flatten.uniq
18
- end
19
-
20
21
  def find(id_or_all)
21
22
  return all if id_or_all == :all
22
- return id_or_all.map { |id| database_servers[id || ::Rails.env] }.compact.uniq if id_or_all.is_a?(Array)
23
+ return id_or_all.filter_map { |id| database_servers[id || ::Rails.env] }.uniq if id_or_all.is_a?(Array)
23
24
 
24
25
  database_servers[id_or_all || ::Rails.env]
25
26
  end
26
27
 
27
28
  def create(settings = {})
28
- raise 'database servers should be set up in database.yml' unless ::Rails.env.test?
29
+ raise "database servers should be set up in database.yml" unless ::Rails.env.test?
29
30
 
30
31
  id = settings[:id]
31
32
  unless id
@@ -38,7 +39,7 @@ module Switchman
38
39
  database_servers[server.id] = server
39
40
  ::ActiveRecord::Base.configurations.configurations <<
40
41
  ::ActiveRecord::DatabaseConfigurations::HashConfig.new(::Rails.env, "#{server.id}/primary", settings)
41
- Shard.send(:initialize_sharding)
42
+ Shard.send(:configure_connects_to)
42
43
  server
43
44
  end
44
45
 
@@ -49,31 +50,48 @@ module Switchman
49
50
  servers[rand(servers.length)]
50
51
  end
51
52
 
53
+ def guard_servers
54
+ all.each { |db| db.guard! if db.config[:prefer_secondary] }
55
+ end
56
+
57
+ def regions
58
+ @regions ||= all.filter_map(&:region).uniq.sort
59
+ end
60
+
52
61
  private
53
62
 
54
63
  def reference_role(role)
55
64
  return if all_roles.include?(role)
56
65
 
57
66
  @all_roles << role
58
- Shard.send(:initialize_sharding)
67
+ Shard.send(:configure_connects_to)
59
68
  end
60
69
 
61
70
  def database_servers
62
- unless @database_servers
71
+ if !@database_servers || @database_servers.empty?
63
72
  @database_servers = {}.with_indifferent_access
73
+ roles = []
64
74
  ::ActiveRecord::Base.configurations.configurations.each do |config|
65
- if config.name.include?('/')
66
- name, role = config.name.split('/')
75
+ if config.name.include?("/")
76
+ name, role = config.name.split("/")
67
77
  else
68
78
  name, role = config.env_name, config.name
69
79
  end
80
+ role = role.to_sym
70
81
 
71
- if role == 'primary'
82
+ roles << role
83
+ if role == :primary
72
84
  @database_servers[name] = DatabaseServer.new(config.env_name, config.configuration_hash)
73
85
  else
74
86
  @database_servers[name].roles << role
75
87
  end
76
88
  end
89
+ # Do this after so that all database servers for all roles are established and we won't prematurely
90
+ # configure a connection for the wrong role
91
+ @all_roles = roles.uniq
92
+ return @database_servers if @database_servers.empty?
93
+
94
+ Shard.send(:configure_connects_to)
77
95
  end
78
96
  @database_servers
79
97
  end
@@ -89,21 +107,22 @@ module Switchman
89
107
  end
90
108
 
91
109
  def connects_to_hash
92
- self.class.all_roles.map do |role|
110
+ self.class.all_roles.to_h do |role|
93
111
  config_role = role
94
112
  config_role = :primary unless roles.include?(role)
95
113
  config_name = :"#{id}/#{config_role}"
96
114
  config_name = :primary if id == ::Rails.env && config_role == :primary
97
115
  [role.to_sym, config_name]
98
- end.to_h
116
+ end
99
117
  end
100
118
 
101
119
  def destroy
102
120
  self.class.send(:database_servers).delete(id) if id
103
121
  Shard.sharded_models.each do |klass|
104
122
  self.class.all_roles.each do |role|
105
- klass.connection_handler.remove_connection_pool(klass.connection_specification_name, role: role,
106
- shard: id.to_sym)
123
+ klass.connection_handler.remove_connection_pool(klass.connection_specification_name,
124
+ role:,
125
+ shard: id.to_sym)
107
126
  end
108
127
  end
109
128
  end
@@ -129,32 +148,57 @@ module Switchman
129
148
  end
130
149
  end
131
150
 
132
- def guard_rail_environment
133
- @guard_rail_environment || ::GuardRail.environment
151
+ def region
152
+ config[:region]
153
+ end
154
+
155
+ # @param region [String, Array<String>] the region(s) to check against
156
+ # @return true if the database server doesn't have a region, or it
157
+ # matches the specified region
158
+ def in_region?(region)
159
+ !self.region || (region.is_a?(Array) ? region.include?(self.region) : self.region == region)
160
+ end
161
+
162
+ # @return true if the database server doesn't have a region, Switchman is
163
+ # not configured with a region, or the database server's region matches
164
+ # Switchman's current region
165
+ def in_current_region?
166
+ unless instance_variable_defined?(:@in_current_region)
167
+ @in_current_region = !region ||
168
+ !Switchman.region ||
169
+ region == Switchman.region
170
+ end
171
+ @in_current_region
134
172
  end
135
173
 
136
174
  # locks this db to a specific environment, except for
137
175
  # when doing writes (then it falls back to the current
138
176
  # value of GuardRail.environment)
139
177
  def guard!(environment = :secondary)
140
- @guard_rail_environment = environment
178
+ DatabaseServer.send(:reference_role, environment)
179
+ ::ActiveRecord::Base.connected_to_stack << { shard_roles: { id.to_sym => environment },
180
+ klasses: [::ActiveRecord::Base] }
141
181
  end
142
182
 
143
183
  def unguard!
144
- @guard_rail_environment = nil
184
+ ::ActiveRecord::Base.connected_to_stack << { shard_roles: { id.to_sym => :_switchman_inherit },
185
+ klasses: [::ActiveRecord::Base] }
145
186
  end
146
187
 
147
188
  def unguard
148
- old_env = @guard_rail_environment
149
- unguard!
150
- yield
151
- ensure
152
- guard!(old_env)
189
+ return yield unless ::ActiveRecord::Base.role_overriden?(id.to_sym)
190
+
191
+ begin
192
+ unguard!
193
+ yield
194
+ ensure
195
+ ::ActiveRecord::Base.connected_to_stack.pop
196
+ end
153
197
  end
154
198
 
155
199
  def shards
156
200
  if id == ::Rails.env
157
- Shard.where('database_server_id IS NULL OR database_server_id=?', id)
201
+ Shard.where("database_server_id IS NULL OR database_server_id=?", id)
158
202
  else
159
203
  Shard.where(database_server_id: id)
160
204
  end
@@ -174,70 +218,66 @@ module Switchman
174
218
  if config_create_statement
175
219
  create_commands = Array(config_create_statement).dup
176
220
  create_statement = lambda {
177
- create_commands.map { |statement| format(statement, name: name, password: password) }
221
+ create_commands.map { |statement| format(statement, name:, password:) }
178
222
  }
179
223
  end
180
224
 
181
225
  id ||= begin
182
- id_seq = Shard.connection.quote(Shard.connection.quote_table_name('switchman_shards_id_seq'))
226
+ id_seq = Shard.connection.quote(Shard.connection.quote_table_name("switchman_shards_id_seq"))
183
227
  next_id = Shard.connection.select_value("SELECT nextval(#{id_seq})")
184
228
  next_id.to_i
185
229
  end
186
230
 
187
231
  name ||= "#{config[:database]}_shard_#{id}"
188
232
 
233
+ schema_already_existed = false
234
+ shard = nil
189
235
  Shard.connection.transaction do
190
- shard = Shard.create!(id: id,
191
- name: name,
192
- database_server_id: self.id)
193
- schema_already_existed = false
194
-
195
- begin
196
- self.class.creating_new_shard = true
197
- DatabaseServer.send(:reference_role, :deploy)
198
- ::ActiveRecord::Base.connected_to(shard: self.id.to_sym, role: :deploy) do
199
- if create_statement
200
- if ::ActiveRecord::Base.connection.select_value("SELECT 1 FROM pg_namespace WHERE nspname=#{::ActiveRecord::Base.connection.quote(name)}")
201
- schema_already_existed = true
202
- raise 'This schema already exists; cannot overwrite'
203
- end
204
- Array(create_statement.call).each do |stmt|
205
- ::ActiveRecord::Base.connection.execute(stmt)
206
- end
236
+ self.class.creating_new_shard = true
237
+ DatabaseServer.send(:reference_role, :deploy)
238
+ ::ActiveRecord::Base.connected_to(shard: self.id.to_sym, role: :deploy) do
239
+ shard = Shard.create!(id:,
240
+ name:,
241
+ database_server_id: self.id)
242
+ if create_statement
243
+ if ::ActiveRecord::Base.connection.select_value(
244
+ "SELECT 1 FROM pg_namespace WHERE nspname=#{::ActiveRecord::Base.connection.quote(name)}"
245
+ )
246
+ schema_already_existed = true
247
+ raise "This schema already exists; cannot overwrite"
207
248
  end
208
- if config[:adapter] == 'postgresql'
209
- old_proc = ::ActiveRecord::Base.connection.raw_connection.set_notice_processor do
210
- end
249
+ Array(create_statement.call).each do |stmt|
250
+ ::ActiveRecord::Base.connection.execute(stmt)
211
251
  end
212
- old_verbose = ::ActiveRecord::Migration.verbose
213
- ::ActiveRecord::Migration.verbose = false
214
-
215
- unless schema == false
216
- shard.activate(*Shard.sharded_models) do
217
- reset_column_information
218
-
219
- ::ActiveRecord::Base.connection.transaction(requires_new: true) do
220
- ::ActiveRecord::Base.connection.migration_context.migrate
221
- end
222
- reset_column_information
223
- ::ActiveRecord::Base.descendants.reject do |m|
224
- m <= UnshardedRecord || !m.table_exists?
225
- end.each(&:define_attribute_methods)
252
+ end
253
+ if config[:adapter] == "postgresql"
254
+ old_proc = ::ActiveRecord::Base.connection.raw_connection.set_notice_processor {}
255
+ end
256
+ old_verbose = ::ActiveRecord::Migration.verbose
257
+ ::ActiveRecord::Migration.verbose = false
258
+
259
+ unless schema == false
260
+ shard.activate do
261
+ ::ActiveRecord::Base.connection.transaction(requires_new: true) do
262
+ ::ActiveRecord::MigrationContext.new(::ActiveRecord::Migrator.migrations_paths).migrate
226
263
  end
264
+
265
+ ::ActiveRecord::Base.descendants.reject do |m|
266
+ m <= UnshardedRecord || !m.table_exists?
267
+ end.each(&:define_attribute_methods)
227
268
  end
228
- ensure
229
- ::ActiveRecord::Migration.verbose = old_verbose
230
- ::ActiveRecord::Base.connection.raw_connection.set_notice_processor(&old_proc) if old_proc
231
269
  end
232
- shard
233
- rescue
234
- shard.destroy
235
- shard.drop_database rescue nil unless schema_already_existed
236
- reset_column_information unless schema == false rescue nil
237
- raise
238
270
  ensure
239
- self.class.creating_new_shard = false
271
+ ::ActiveRecord::Migration.verbose = old_verbose
272
+ ::ActiveRecord::Base.connection.raw_connection.set_notice_processor(&old_proc) if old_proc
240
273
  end
274
+ shard
275
+ rescue
276
+ shard&.destroy
277
+ shard&.drop_database rescue nil unless schema_already_existed
278
+ raise
279
+ ensure
280
+ self.class.creating_new_shard = false
241
281
  end
242
282
  end
243
283
 
@@ -259,18 +299,18 @@ module Switchman
259
299
  end
260
300
 
261
301
  def primary_shard
262
- unless instance_variable_defined?(:@primary_shard)
263
- # if sharding isn't fully set up yet, we may not be able to query the shards table
264
- @primary_shard = Shard.default if Shard.default.database_server == self
265
- @primary_shard ||= shards.where(name: nil).first
266
- end
267
- @primary_shard
268
- end
302
+ return nil unless primary_shard_id
269
303
 
270
- private
304
+ Shard.lookup(primary_shard_id)
305
+ end
271
306
 
272
- def reset_column_information
273
- ::ActiveRecord::Base.descendants.reject { |m| m <= UnshardedRecord }.each(&:reset_column_information)
307
+ def primary_shard_id
308
+ unless instance_variable_defined?(:@primary_shard_id)
309
+ # if sharding isn't fully set up yet, we may not be able to query the shards table
310
+ @primary_shard_id = Shard.default.id if Shard.default.database_server == self
311
+ @primary_shard_id ||= shards.where(name: nil).first&.id
312
+ end
313
+ @primary_shard_id
274
314
  end
275
315
  end
276
316
  end
@@ -1,13 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'switchman/database_server'
4
-
5
3
  module Switchman
6
4
  class DefaultShard
7
5
  def id
8
- 'default'
6
+ "default"
9
7
  end
10
- alias cache_key id
8
+ alias_method :cache_key, :id
9
+
11
10
  def activate(*_classes)
12
11
  yield
13
12
  end
@@ -59,8 +58,18 @@ module Switchman
59
58
  self
60
59
  end
61
60
 
61
+ def region; end
62
+
63
+ def in_region?(_region)
64
+ true
65
+ end
66
+
67
+ def in_current_region?
68
+ true
69
+ end
70
+
62
71
  def _dump(_depth)
63
- ''
72
+ ""
64
73
  end
65
74
 
66
75
  def self._load(_str)
@@ -4,105 +4,22 @@ module Switchman
4
4
  class Engine < ::Rails::Engine
5
5
  isolate_namespace Switchman
6
6
 
7
- # enable Rails 6.1 style connection handling
8
- config.active_record.legacy_connection_handling = false
9
7
  config.active_record.writing_role = :primary
10
8
 
11
- config.autoload_once_paths << File.expand_path('app/models', config.paths.path)
9
+ ::GuardRail.singleton_class.prepend(GuardRail::ClassMethods)
12
10
 
13
- def self.lookup_stores(cache_store_config)
14
- result = {}
15
- cache_store_config.each do |key, value|
16
- next if value.is_a?(String)
17
-
18
- result[key] = ::ActiveSupport::Cache.lookup_store(value)
19
- end
20
-
21
- cache_store_config.each do |key, value| # rubocop:disable Style/CombinableLoops
22
- next unless value.is_a?(String)
23
-
24
- result[key] = result[value]
25
- end
26
- result
27
- end
28
-
29
- initializer 'switchman.initialize_cache', before: 'initialize_cache' do
30
- require 'switchman/active_support/cache'
31
- ::ActiveSupport::Cache.singleton_class.prepend(ActiveSupport::Cache::ClassMethods)
32
-
33
- # if we haven't already setup our cache map out-of-band, set it up from
34
- # config.cache_store now. behaves similarly to Rails' default
35
- # initialize_cache initializer, but for each value in the map, rather
36
- # than just Rails.cache. if config.cache_store is a flat value, uses it
37
- # to fill just the Rails.env entry in the cache map.
38
- unless Switchman.config[:cache_map].present?
39
- cache_store_config = ::Rails.configuration.cache_store
40
- cache_store_config = { ::Rails.env => cache_store_config } unless cache_store_config.is_a?(Hash)
41
-
42
- Switchman.config[:cache_map] = Engine.lookup_stores(cache_store_config)
43
- end
44
-
45
- # if the configured cache map (either from before, or as populated from
46
- # config.cache_store) didn't have an entry for Rails.env, add one using
47
- # lookup_store(nil); matches the behavior of Rails' default
48
- # initialize_cache initializer when config.cache_store is nil.
49
- unless Switchman.config[:cache_map].key?(::Rails.env)
50
- value = ::ActiveSupport::Cache.lookup_store(nil)
51
- Switchman.config[:cache_map][::Rails.env] = value
52
- end
53
-
54
- middlewares = Switchman.config[:cache_map].values.map do |store|
55
- store.middleware if store.respond_to?(:middleware)
56
- end.compact.uniq
57
- middlewares.each do |middleware|
58
- config.middleware.insert_before('Rack::Runtime', middleware)
59
- end
60
-
61
- # prevent :initialize_cache from trying to (or needing to) set
62
- # Rails.cache. once our switchman.extend_ar initializer (below) runs
63
- # Rails.cache will be overridden to pull appropriate values from the
64
- # cache map, but between now and then, Rails.cache should return the
65
- # Rails.env entry in the cache map.
66
- ::Rails.cache = Switchman.config[:cache_map][::Rails.env]
67
- end
68
-
69
- initializer 'switchman.extend_ar', before: 'active_record.initialize_database' do
11
+ initializer "switchman.active_record_patch",
12
+ before: "active_record.initialize_database",
13
+ after: :setup_once_autoloader do
70
14
  ::ActiveSupport.on_load(:active_record) do
71
- require 'switchman/active_record/abstract_adapter'
72
- require 'switchman/active_record/association'
73
- require 'switchman/active_record/attribute_methods'
74
- require 'switchman/active_record/base'
75
- require 'switchman/active_record/calculations'
76
- require 'switchman/active_record/connection_pool'
77
- require 'switchman/active_record/database_configurations'
78
- require 'switchman/active_record/database_configurations/database_config'
79
- require 'switchman/active_record/finder_methods'
80
- require 'switchman/active_record/log_subscriber'
81
- require 'switchman/active_record/migration'
82
- require 'switchman/active_record/model_schema'
83
- require 'switchman/active_record/persistence'
84
- require 'switchman/active_record/predicate_builder'
85
- require 'switchman/active_record/query_cache'
86
- require 'switchman/active_record/query_methods'
87
- require 'switchman/active_record/reflection'
88
- require 'switchman/active_record/relation'
89
- require 'switchman/active_record/spawn_methods'
90
- require 'switchman/active_record/statement_cache'
91
- require 'switchman/active_record/tasks/database_tasks'
92
- require 'switchman/active_record/type_caster'
93
- require 'switchman/arel'
94
- require 'switchman/call_super'
95
- require 'switchman/rails'
96
- require 'switchman/guard_rail/relation'
97
- require 'switchman/standard_error'
98
-
99
- ::StandardError.include(StandardError)
15
+ # Switchman requires postgres, so just always load the pg adapter
16
+ require "active_record/connection_adapters/postgresql_adapter"
100
17
 
101
18
  self.default_shard = ::Rails.env.to_sym
102
19
  self.default_role = :primary
103
20
 
104
- include ActiveRecord::Base
105
- include ActiveRecord::AttributeMethods
21
+ prepend ActiveRecord::Base
22
+ prepend ActiveRecord::AttributeMethods
106
23
  include ActiveRecord::Persistence
107
24
  singleton_class.prepend ActiveRecord::ModelSchema::ClassMethods
108
25
 
@@ -111,25 +28,40 @@ module Switchman
111
28
  ::ActiveRecord::StatementCache::BindMap.prepend(ActiveRecord::StatementCache::BindMap)
112
29
  ::ActiveRecord::StatementCache::Substitute.send(:attr_accessor, :primary, :sharded)
113
30
 
114
- ::ActiveRecord::Associations::CollectionAssociation.prepend(ActiveRecord::CollectionAssociation)
115
- ::ActiveRecord::Associations::HasOneAssociation.prepend(ActiveRecord::ForeignAssociation)
116
- ::ActiveRecord::Associations::HasManyAssociation.prepend(ActiveRecord::ForeignAssociation)
31
+ ::ActiveRecord::Associations::CollectionAssociation.prepend(ActiveRecord::Associations::CollectionAssociation)
32
+ ::ActiveRecord::Associations::HasOneAssociation.prepend(ActiveRecord::Associations::ForeignAssociation)
33
+ ::ActiveRecord::Associations::HasManyAssociation.prepend(ActiveRecord::Associations::ForeignAssociation)
117
34
 
118
35
  ::ActiveRecord::PredicateBuilder.singleton_class.prepend(ActiveRecord::PredicateBuilder)
119
36
 
120
- prepend(ActiveRecord::AutosaveAssociation)
37
+ prepend(ActiveRecord::Associations::AutosaveAssociation)
121
38
 
122
- ::ActiveRecord::Associations::Association.prepend(ActiveRecord::Association)
123
- ::ActiveRecord::Associations::BelongsToAssociation.prepend(ActiveRecord::BelongsToAssociation)
124
- ::ActiveRecord::Associations::CollectionProxy.include(ActiveRecord::CollectionProxy)
39
+ ::ActiveRecord::Associations::Association.prepend(ActiveRecord::Associations::Association)
40
+ ::ActiveRecord::Associations::BelongsToAssociation.prepend(ActiveRecord::Associations::BelongsToAssociation)
41
+ ::ActiveRecord::Associations::CollectionProxy.include(ActiveRecord::Associations::CollectionProxy)
125
42
 
126
- ::ActiveRecord::Associations::Preloader::Association.prepend(ActiveRecord::Preloader::Association)
43
+ ::ActiveRecord::Associations::Preloader::Association.prepend(ActiveRecord::Associations::Preloader::Association)
44
+ ::ActiveRecord::Associations::Preloader::Association::LoaderRecords.prepend(
45
+ ActiveRecord::Associations::Preloader::Association::LoaderRecords
46
+ )
127
47
  ::ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(ActiveRecord::AbstractAdapter)
48
+ ::ActiveRecord::ConnectionAdapters::ConnectionHandler.prepend(ActiveRecord::ConnectionHandler)
128
49
  ::ActiveRecord::ConnectionAdapters::ConnectionPool.prepend(ActiveRecord::ConnectionPool)
129
50
  ::ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(ActiveRecord::QueryCache)
51
+ ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(ActiveRecord::PostgreSQLAdapter)
52
+ # https://github.com/rails/rails/commit/0016280f4fde55d96738887093dc333aae0d107b
53
+ if ::Rails.version < "7.2"
54
+ ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(ActiveRecord::PostgreSQLAdapter::ClassMethods)
55
+ else
56
+ ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.singleton_class.prepend(
57
+ ActiveRecord::PostgreSQLAdapter::ClassMethods
58
+ )
59
+ end
130
60
 
131
61
  ::ActiveRecord::DatabaseConfigurations.prepend(ActiveRecord::DatabaseConfigurations)
132
- ::ActiveRecord::DatabaseConfigurations::DatabaseConfig.prepend(ActiveRecord::DatabaseConfigurations::DatabaseConfig)
62
+ ::ActiveRecord::DatabaseConfigurations::DatabaseConfig.prepend(
63
+ ActiveRecord::DatabaseConfigurations::DatabaseConfig
64
+ )
133
65
 
134
66
  ::ActiveRecord::LogSubscriber.prepend(ActiveRecord::LogSubscriber)
135
67
  ::ActiveRecord::Migration.prepend(ActiveRecord::Migration)
@@ -137,6 +69,11 @@ module Switchman
137
69
  ::ActiveRecord::MigrationContext.prepend(ActiveRecord::MigrationContext)
138
70
  ::ActiveRecord::Migrator.prepend(ActiveRecord::Migrator)
139
71
 
72
+ if ::Rails.version > "7.1.3"
73
+ ::ActiveRecord::PendingMigrationConnection.singleton_class
74
+ .include(ActiveRecord::PendingMigrationConnection::ClassMethods)
75
+ end
76
+
140
77
  ::ActiveRecord::Reflection::AbstractReflection.include(ActiveRecord::Reflection::AbstractReflection)
141
78
  ::ActiveRecord::Reflection::AssociationReflection.prepend(ActiveRecord::Reflection::AssociationScopeCache)
142
79
  ::ActiveRecord::Reflection::ThroughReflection.prepend(ActiveRecord::Reflection::AssociationScopeCache)
@@ -146,64 +83,81 @@ module Switchman
146
83
  ::ActiveRecord::Relation.include(ActiveRecord::QueryMethods)
147
84
  ::ActiveRecord::Relation.prepend(GuardRail::Relation)
148
85
  ::ActiveRecord::Relation.prepend(ActiveRecord::Relation)
86
+ ::ActiveRecord::Relation.prepend(ActiveRecord::Relation::InsertUpsertAll) if ::Rails.version >= "7.2"
149
87
  ::ActiveRecord::Relation.include(ActiveRecord::SpawnMethods)
150
88
  ::ActiveRecord::Relation.include(CallSuper)
151
89
 
152
- ::ActiveRecord::PredicateBuilder::AssociationQueryValue.prepend(ActiveRecord::PredicateBuilder::AssociationQueryValue)
90
+ ::ActiveRecord::PredicateBuilder::PolymorphicArrayValue.prepend(
91
+ ActiveRecord::PredicateBuilder::PolymorphicArrayValue
92
+ )
153
93
 
154
94
  ::ActiveRecord::Tasks::DatabaseTasks.singleton_class.prepend(ActiveRecord::Tasks::DatabaseTasks)
155
95
 
96
+ ::ActiveRecord::TestFixtures.prepend(ActiveRecord::TestFixtures)
97
+
156
98
  ::ActiveRecord::TypeCaster::Map.include(ActiveRecord::TypeCaster::Map)
157
99
  ::ActiveRecord::TypeCaster::Connection.include(ActiveRecord::TypeCaster::Connection)
158
100
 
159
- ::Rails.singleton_class.prepend(Rails::ClassMethods)
160
-
161
101
  ::Arel::Table.prepend(Arel::Table)
162
102
  ::Arel::Visitors::ToSql.prepend(Arel::Visitors::ToSql)
163
- end
164
- end
165
-
166
- def self.foreign_key_check(name, type, limit: nil)
167
- puts "WARNING: All foreign keys need to be 8-byte integers. #{name} looks like a foreign key. If so, please add the option: `:limit => 8`" if name.to_s =~ /_id\z/ && type.to_s == 'integer' && limit.to_i < 8
168
- end
169
103
 
170
- initializer 'switchman.extend_connection_adapters', after: 'active_record.initialize_database' do
171
- ::ActiveSupport.on_load(:active_record) do
172
104
  ::ActiveRecord::ConnectionAdapters::AbstractAdapter.descendants.each do |klass|
173
105
  klass.prepend(ActiveRecord::AbstractAdapter::ForeignKeyCheck)
174
106
  end
175
107
 
176
- require 'switchman/active_record/table_definition'
177
108
  ::ActiveRecord::ConnectionAdapters::TableDefinition.prepend(ActiveRecord::TableDefinition)
178
-
179
- if defined?(::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
180
- require 'switchman/active_record/postgresql_adapter'
181
- ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(ActiveRecord::PostgreSQLAdapter)
182
- end
183
-
184
- Shard.send(:initialize_sharding)
185
109
  end
110
+ # Ensure that ActiveRecord::Base is always loaded before any app-level
111
+ # initializers can go try to load Switchman::Shard or we get a loop
112
+ ::ActiveRecord::Base
186
113
  end
187
114
 
188
- initializer 'switchman.eager_load' do
189
- ::ActiveSupport.on_load(:before_eager_load) do
190
- # This needs to be loaded before Switchman::Shard, otherwise it won't autoload it correctly
191
- require 'active_record/base'
115
+ initializer "switchman.error_patch", after: "active_record.initialize_database" do
116
+ ::ActiveSupport.on_load(:active_record) do
117
+ ::StandardError.include(StandardError)
192
118
  end
193
119
  end
194
120
 
195
- initializer 'switchman.extend_guard_rail', before: 'switchman.extend_ar' do
196
- ::ActiveSupport.on_load(:active_record) do
197
- require 'switchman/guard_rail'
121
+ initializer "switchman.initialize_cache", before: :initialize_cache, after: "active_record.initialize_database" do
122
+ ::ActiveSupport::Cache.singleton_class.prepend(ActiveSupport::Cache::ClassMethods)
123
+
124
+ # if we haven't already setup our cache map out-of-band, set it up from
125
+ # config.cache_store now. behaves similarly to Rails' default
126
+ # initialize_cache initializer, but for each value in the map, rather
127
+ # than just Rails.cache. if config.cache_store is a flat value, uses it
128
+ # to fill just the Rails.env entry in the cache map.
129
+ unless Switchman.config[:cache_map].present?
130
+ cache_store_config = ::Rails.configuration.cache_store
131
+ cache_store_config = { ::Rails.env => cache_store_config } unless cache_store_config.is_a?(Hash)
198
132
 
199
- ::GuardRail.singleton_class.prepend(GuardRail::ClassMethods)
133
+ Switchman.config[:cache_map] = ::ActiveSupport::Cache.lookup_stores(cache_store_config)
200
134
  end
201
- end
202
135
 
203
- initializer 'switchman.extend_controller', after: 'guard_rail.extend_ar' do
204
- ::ActiveSupport.on_load(:action_controller) do
205
- require 'switchman/action_controller/caching'
136
+ # if the configured cache map (either from before, or as populated from
137
+ # config.cache_store) didn't have an entry for Rails.env, add one using
138
+ # lookup_store(nil); matches the behavior of Rails' default
139
+ # initialize_cache initializer when config.cache_store is nil.
140
+ unless Switchman.config[:cache_map].key?(::Rails.env)
141
+ value = ::ActiveSupport::Cache.lookup_store(nil)
142
+ Switchman.config[:cache_map][::Rails.env] = value
143
+ end
144
+
145
+ middlewares = Switchman.config[:cache_map].values.filter_map do |store|
146
+ store.middleware if store.respond_to?(:middleware)
147
+ end.uniq
148
+ middlewares.each do |middleware|
149
+ config.middleware.insert_before("Rack::Runtime", middleware)
150
+ end
206
151
 
152
+ # prevent :initialize_cache from trying to (or needing to) set
153
+ # Rails.cache. once our switchman.extend_ar initializer (below) runs
154
+ # Rails.cache will be overridden to pull appropriate values from the
155
+ # cache map, but between now and then, Rails.cache should return the
156
+ # Rails.env entry in the cache map.
157
+ ::Rails.cache = Switchman.config[:cache_map][::Rails.env]
158
+ ::Rails.singleton_class.prepend(Rails::ClassMethods)
159
+
160
+ ::ActiveSupport.on_load(:action_controller) do
207
161
  ::ActionController::Base.include(ActionController::Caching)
208
162
  end
209
163
  end