switchman 3.0.5 → 4.0.0

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 +6 -15
  8. data/lib/switchman/active_record/associations.rb +331 -0
  9. data/lib/switchman/active_record/attribute_methods.rb +182 -77
  10. data/lib/switchman/active_record/base.rb +249 -46
  11. data/lib/switchman/active_record/calculations.rb +98 -44
  12. data/lib/switchman/active_record/connection_handler.rb +18 -0
  13. data/lib/switchman/active_record/connection_pool.rb +27 -28
  14. data/lib/switchman/active_record/database_configurations.rb +44 -6
  15. data/lib/switchman/active_record/finder_methods.rb +46 -16
  16. data/lib/switchman/active_record/log_subscriber.rb +11 -5
  17. data/lib/switchman/active_record/migration.rb +52 -5
  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 +12 -11
  22. data/lib/switchman/active_record/predicate_builder.rb +2 -2
  23. data/lib/switchman/active_record/query_cache.rb +49 -20
  24. data/lib/switchman/active_record/query_methods.rb +202 -136
  25. data/lib/switchman/active_record/reflection.rb +1 -1
  26. data/lib/switchman/active_record/relation.rb +40 -28
  27. data/lib/switchman/active_record/spawn_methods.rb +2 -2
  28. data/lib/switchman/active_record/statement_cache.rb +11 -7
  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 +53 -0
  32. data/lib/switchman/active_support/cache.rb +25 -4
  33. data/lib/switchman/arel.rb +45 -7
  34. data/lib/switchman/call_super.rb +2 -2
  35. data/lib/switchman/database_server.rb +123 -79
  36. data/lib/switchman/default_shard.rb +14 -5
  37. data/lib/switchman/engine.rb +79 -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 +226 -241
  46. data/lib/switchman/sharded_instrumenter.rb +3 -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 +2 -2
  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 +50 -58
  55. data/lib/switchman/active_record/association.rb +0 -206
  56. data/lib/switchman/open4.rb +0 -80
@@ -1,20 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'switchman/errors'
4
-
5
3
  module Switchman
6
4
  module ActiveRecord
7
5
  module ConnectionPool
8
- def shard
9
- shard_stack.last || Shard.default
10
- end
6
+ if ::Rails.version < "7.1"
7
+ def get_schema_cache(connection)
8
+ self.schema_cache ||= SharedSchemaCache.get_schema_cache(connection)
9
+ self.schema_cache.connection = connection
11
10
 
12
- def shard_stack
13
- unless (shard_stack = Thread.current.thread_variable_get(tls_key))
14
- shard_stack = Concurrent::Array.new
15
- Thread.current.thread_variable_set(tls_key, shard_stack)
11
+ self.schema_cache
16
12
  end
17
- shard_stack
13
+
14
+ # rubocop:disable Naming/AccessorMethodName override method
15
+ def set_schema_cache(cache)
16
+ schema_cache = get_schema_cache(cache.connection)
17
+
18
+ cache.instance_variables.each do |x|
19
+ schema_cache.instance_variable_set(x, cache.instance_variable_get(x))
20
+ end
21
+ end
22
+ # rubocop:enable Naming/AccessorMethodName override method
18
23
  end
19
24
 
20
25
  def default_schema
@@ -26,15 +31,15 @@ module Switchman
26
31
 
27
32
  def checkout_new_connection
28
33
  conn = super
29
- conn.shard = shard
34
+ conn.shard = current_shard
30
35
  conn
31
36
  end
32
37
 
33
38
  def connection(switch_shard: true)
34
39
  conn = super()
35
- raise NonExistentShardError if shard.new_record?
40
+ raise Errors::NonExistentShardError if current_shard.new_record?
36
41
 
37
- switch_database(conn) if conn.shard != shard && switch_shard
42
+ switch_database(conn) if conn.shard != current_shard && switch_shard
38
43
  conn
39
44
  end
40
45
 
@@ -44,28 +49,22 @@ module Switchman
44
49
  flush
45
50
  end
46
51
 
47
- def remove_shard!(shard)
48
- synchronize do
49
- # The shard might be currently active, so we need to update our own shard
50
- self.shard = Shard.default if self.shard == shard
51
- # Update out any connections that may be using this shard
52
- @connections.each do |conn|
53
- # This will also update the connection's shard to the default shard
54
- switch_database(conn) if conn.shard == shard
55
- end
56
- end
57
- end
58
-
59
52
  def switch_database(conn)
60
- @schemas = conn.current_schemas if !@schemas && conn.adapter_name == 'PostgreSQL' && !shard.database_server.config[:shard_name]
53
+ if !@schemas && conn.adapter_name == "PostgreSQL" && !current_shard.database_server.config[:shard_name]
54
+ @schemas = conn.current_schemas
55
+ end
61
56
 
62
- conn.shard = shard
57
+ conn.shard = current_shard
63
58
  end
64
59
 
65
60
  private
66
61
 
62
+ def current_shard
63
+ connection_class.current_switchman_shard
64
+ end
65
+
67
66
  def tls_key
68
- "#{object_id}_shard".to_sym
67
+ :"#{object_id}_shard"
69
68
  end
70
69
  end
71
70
  end
@@ -3,6 +3,32 @@
3
3
  module Switchman
4
4
  module ActiveRecord
5
5
  module DatabaseConfigurations
6
+ # key difference: For each env name, ensure only one writable config is returned
7
+ # since all should point to the same data, even if multiple are writable
8
+ # (Picks 'primary' since it is guaranteed to exist and switchman handles activating
9
+ # deploy through other means)
10
+ if ::Rails.version < "7.1"
11
+ def configs_for(include_replicas: false, name: nil, **)
12
+ res = super
13
+ if name && !include_replicas
14
+ return nil unless name.end_with?("primary")
15
+ elsif !include_replicas
16
+ return res.select { |config| config.name.end_with?("primary") }
17
+ end
18
+ res
19
+ end
20
+ else
21
+ def configs_for(include_hidden: false, name: nil, **)
22
+ res = super
23
+ if name && !include_hidden
24
+ return nil unless name.end_with?("primary")
25
+ elsif !include_hidden
26
+ return res.select { |config| config.name.end_with?("primary") }
27
+ end
28
+ res
29
+ end
30
+ end
31
+
6
32
  private
7
33
 
8
34
  # key difference: assumes a hybrid two-tier structure; each third tier
@@ -13,19 +39,31 @@ module Switchman
13
39
  return configs if configs.is_a?(Array)
14
40
 
15
41
  db_configs = configs.flat_map do |env_name, config|
16
- roles = config.keys.select { |k| config[k].is_a?(Hash) }
17
- base_config = config.except(*roles)
42
+ if config.is_a?(Hash)
43
+ # It would be nice to do the auto-fallback that we want here, but we haven't
44
+ # actually done that for years (or maybe ever) and it will be a big lift to get working
45
+ roles = config.keys.select do |k|
46
+ config[k].is_a?(Hash) || (config[k].is_a?(Array) && config[k].all?(Hash))
47
+ end
48
+ base_config = config.except(*roles)
49
+ else
50
+ base_config = config
51
+ roles = []
52
+ end
18
53
 
19
54
  name = "#{env_name}/primary"
20
- name = 'primary' if env_name == default_env
55
+ name = "primary" if env_name == default_env
21
56
  base_db = build_db_config_from_raw_config(env_name, name, base_config)
22
57
  [base_db] + roles.map do |role|
23
- build_db_config_from_raw_config(env_name, "#{env_name}/#{role}",
24
- base_config.merge(config[role]).merge(replica: true))
58
+ build_db_config_from_raw_config(
59
+ env_name,
60
+ "#{env_name}/#{role}",
61
+ base_config.merge(config[role].is_a?(Array) ? config[role].first : config[role])
62
+ )
25
63
  end
26
64
  end
27
65
 
28
- db_configs << environment_url_config(default_env, 'primary', {}) unless db_configs.find(&:for_current_env?)
66
+ db_configs << environment_url_config(default_env, "primary", {}) unless db_configs.find(&:for_current_env?)
29
67
 
30
68
  merge_db_environment_variables(default_env, db_configs.compact)
31
69
  end
@@ -7,7 +7,7 @@ module Switchman
7
7
  return super(id) unless klass.integral_id?
8
8
 
9
9
  if shard_source_value != :implicit
10
- current_shard = Shard.current(klass.connection_classes)
10
+ current_shard = Shard.current(klass.connection_class_for_self)
11
11
  result = activate do |relation, shard|
12
12
  current_id = Shard.relative_id_for(id, current_shard, shard)
13
13
  # current_id will be nil for non-integral id
@@ -33,7 +33,7 @@ module Switchman
33
33
  end
34
34
 
35
35
  def find_some_ordered(ids)
36
- current_shard = Shard.current(klass.connection_classes)
36
+ current_shard = Shard.current(klass.connection_class_for_self)
37
37
  ids = ids.map { |id| Shard.relative_id_for(id, current_shard, current_shard) }
38
38
  super(ids)
39
39
  end
@@ -42,26 +42,56 @@ module Switchman
42
42
  primary_shard.activate { super }
43
43
  end
44
44
 
45
- def exists?(conditions = :none)
46
- conditions = conditions.id if ::ActiveRecord::Base === conditions
47
- return false unless conditions
45
+ if ::Rails.version < "7.1"
46
+ def exists?(conditions = :none)
47
+ conditions = conditions.id if ::ActiveRecord::Base === conditions
48
+ return false unless conditions
48
49
 
49
- relation = apply_join_dependency(eager_loading: false)
50
- return false if ::ActiveRecord::NullRelation === relation
50
+ relation = apply_join_dependency(eager_loading: false)
51
+ return false if ::ActiveRecord::NullRelation === relation
51
52
 
52
- relation = relation.except(:select, :order).select('1 AS one').limit(1)
53
+ relation = relation.except(:select, :order).select("1 AS one").limit(1)
53
54
 
54
- case conditions
55
- when Array, Hash
56
- relation = relation.where(conditions)
57
- else
58
- relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none
55
+ case conditions
56
+ when Array, Hash
57
+ relation = relation.where(conditions)
58
+ else
59
+ relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none
60
+ end
61
+
62
+ relation.activate do |shard_rel|
63
+ return true if connection.select_value(shard_rel.arel, "#{name} Exists")
64
+ end
65
+ false
59
66
  end
67
+ else
68
+ def exists?(conditions = :none)
69
+ return false if @none
70
+
71
+ if Base === conditions
72
+ raise ArgumentError, <<-TEXT.squish
73
+ You are passing an instance of ActiveRecord::Base to `exists?`.
74
+ Please pass the id of the object by calling `.id`.
75
+ TEXT
76
+ end
60
77
 
61
- relation.activate do |shard_rel|
62
- return true if connection.select_value(shard_rel.arel, "#{name} Exists")
78
+ return false if !conditions || limit_value == 0 # rubocop:disable Style/NumericPredicate
79
+
80
+ if eager_loading?
81
+ relation = apply_join_dependency(eager_loading: false)
82
+ return relation.exists?(conditions)
83
+ end
84
+
85
+ relation = construct_relation_for_exists(conditions)
86
+ return false if relation.where_clause.contradiction?
87
+
88
+ relation.activate do |shard_rel|
89
+ return true if skip_query_cache_if_necessary do
90
+ connection.select_rows(shard_rel.arel, "#{name} Exists?").size == 1
91
+ end
92
+ end
93
+ false
63
94
  end
64
- false
65
95
  end
66
96
  end
67
97
  end
@@ -5,8 +5,10 @@ module Switchman
5
5
  module LogSubscriber
6
6
  # sadly, have to completely replace this
7
7
  def sql(event)
8
- self.class.runtime += event.duration
9
- return unless logger.debug?
8
+ if ::Rails.version < "7.1"
9
+ self.class.runtime += event.duration
10
+ return unless logger.debug?
11
+ end
10
12
 
11
13
  payload = event.payload
12
14
 
@@ -14,20 +16,24 @@ module Switchman
14
16
 
15
17
  name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
16
18
  name = "CACHE #{name}" if payload[:cached]
17
- sql = payload[:sql].squeeze(' ')
19
+ sql = payload[:sql].squeeze(" ")
18
20
  binds = nil
19
21
  shard = payload[:shard]
20
22
  shard = " [#{shard[:database_server_id]}:#{shard[:id]} #{shard[:env]}]" if shard
21
23
 
22
24
  unless (payload[:binds] || []).empty?
23
25
  casted_params = type_casted_binds(payload[:type_casted_binds])
24
- binds = ' ' + payload[:binds].zip(casted_params).map do |attr, value|
26
+ binds = " " + payload[:binds].zip(casted_params).map do |attr, value|
25
27
  render_bind(attr, value)
26
28
  end.inspect
27
29
  end
28
30
 
29
31
  name = colorize_payload_name(name, payload[:name])
30
- sql = color(sql, sql_color(sql), true)
32
+ sql = if ::Rails.version < "7.1"
33
+ color(sql, sql_color(sql), true)
34
+ else
35
+ color(sql, sql_color(sql), bold: true)
36
+ end
31
37
 
32
38
  debug " #{name} #{sql}#{binds}#{shard}"
33
39
  end
@@ -14,16 +14,27 @@ module Switchman
14
14
 
15
15
  def connection
16
16
  conn = super
17
- ::ActiveRecord::Base.connection_pool.switch_database(conn) if conn.shard != ::ActiveRecord::Base.connection_pool.shard
17
+ if conn.shard != ::ActiveRecord::Base.current_switchman_shard
18
+ ::ActiveRecord::Base.connection_pool.switch_database(conn)
19
+ end
18
20
  conn
19
21
  end
20
22
  end
21
23
 
22
24
  module Migrator
23
- # significant change: hash shard name, not database name
25
+ # significant change: use the shard name instead of the database name
26
+ # in the lock id. Especially if you're going through pgbouncer, the
27
+ # database name you're accessing may not be consistent
24
28
  def generate_migrator_advisory_lock_id
25
- shard_name_hash = Zlib.crc32(Shard.current.name)
26
- ::ActiveRecord::Migrator::MIGRATOR_SALT * shard_name_hash
29
+ db_name_hash = Zlib.crc32(Shard.current.name)
30
+ shard_name_hash = ::ActiveRecord::Migrator::MIGRATOR_SALT * db_name_hash
31
+ # Store in internalmetadata to allow other tools to be able to lock out migrations
32
+ if ::Rails.version < "7.1"
33
+ ::ActiveRecord::InternalMetadata[:migrator_advisory_lock_id] = shard_name_hash
34
+ else
35
+ ::ActiveRecord::InternalMetadata.new(connection)[:migrator_advisory_lock_id] = shard_name_hash
36
+ end
37
+ shard_name_hash
27
38
  end
28
39
 
29
40
  # significant change: strip out prefer_secondary from config
@@ -39,13 +50,49 @@ module Switchman
39
50
  end
40
51
 
41
52
  module MigrationContext
53
+ def migrate(...)
54
+ connection = ::ActiveRecord::Base.connection
55
+ schema_cache_holder = ::ActiveRecord::Base.connection_pool
56
+ schema_cache_holder = schema_cache_holder.schema_reflection if ::Rails.version >= "7.1"
57
+ previous_schema_cache = if ::Rails.version < "7.1"
58
+ schema_cache_holder.get_schema_cache(connection)
59
+ else
60
+ schema_cache_holder.instance_variable_get(:@cache)
61
+ end
62
+
63
+ if ::Rails.version < "7.1"
64
+ temporary_schema_cache = ::ActiveRecord::ConnectionAdapters::SchemaCache.new(connection)
65
+
66
+ reset_column_information
67
+ schema_cache_holder.set_schema_cache(temporary_schema_cache)
68
+ else
69
+ schema_cache_holder.instance_variable_get(:@cache)
70
+
71
+ reset_column_information
72
+ schema_cache_holder.clear!
73
+ end
74
+
75
+ begin
76
+ super(...)
77
+ ensure
78
+ schema_cache_holder.set_schema_cache(previous_schema_cache)
79
+ reset_column_information
80
+ end
81
+ end
82
+
42
83
  def migrations
43
84
  return @migrations if instance_variable_defined?(:@migrations)
44
85
 
45
86
  migrations_cache = Thread.current[:migrations_cache] ||= {}
46
- key = Digest::MD5.hexdigest(migration_files.sort.join(','))
87
+ key = Digest::MD5.hexdigest(migration_files.sort.join(","))
47
88
  @migrations = migrations_cache[key] ||= super
48
89
  end
90
+
91
+ private
92
+
93
+ def reset_column_information
94
+ ::ActiveRecord::Base.descendants.reject { |m| m <= UnshardedRecord }.each(&:reset_column_information)
95
+ end
49
96
  end
50
97
  end
51
98
  end
@@ -6,7 +6,7 @@ module Switchman
6
6
  module ClassMethods
7
7
  def quoted_table_name
8
8
  @quoted_table_name ||= {}
9
- @quoted_table_name[Shard.current(connection_classes).id] ||= connection.quote_table_name(table_name)
9
+ @quoted_table_name[Shard.current(connection_class_for_self).id] ||= connection.quote_table_name(table_name)
10
10
  end
11
11
  end
12
12
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Switchman
4
+ module ActiveRecord
5
+ module PendingMigrationConnection
6
+ module ClassMethods
7
+ def current_role
8
+ ::ActiveRecord::Base.current_role
9
+ end
10
+
11
+ def current_switchman_shard
12
+ ::ActiveRecord::Base.current_switchman_shard
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -5,11 +5,46 @@ module Switchman
5
5
  module Persistence
6
6
  # touch reads the id attribute directly, so it's not relative to the current shard
7
7
  def touch(*, **)
8
- shard.activate(self.class.connection_classes) { super }
8
+ writable_shadow_record_warning
9
+ shard.activate(self.class.connection_class_for_self) { super }
9
10
  end
10
11
 
11
12
  def update_columns(*)
12
- shard.activate(self.class.connection_classes) { super }
13
+ writable_shadow_record_warning
14
+ shard.activate(self.class.connection_class_for_self) { super }
15
+ end
16
+
17
+ def delete
18
+ db = shard.database_server
19
+ db.unguard { super }
20
+ end
21
+
22
+ def destroy
23
+ writable_shadow_record_warning
24
+ super
25
+ end
26
+
27
+ def create_or_update(**, &block)
28
+ writable_shadow_record_warning
29
+ super
30
+ end
31
+
32
+ def reload(*)
33
+ res = super
34
+ # When a shadow record is reloaded the real record is returned. So
35
+ # we need to ensure the loaded_from_shard is set correctly after a reload.
36
+ @loaded_from_shard = @shard
37
+ if @readonly_from_shadow
38
+ @readonly_from_shadow = false
39
+ @readonly = false
40
+ end
41
+ res
42
+ end
43
+
44
+ def writable_shadow_record_warning
45
+ return unless shadow_record? && Switchman.config[:writable_shadow_records]
46
+
47
+ Switchman::Deprecation.warn("writing to shadow records is not supported")
13
48
  end
14
49
  end
15
50
  end
@@ -5,9 +5,9 @@ module Switchman
5
5
  module PostgreSQLAdapter
6
6
  # copy/paste; use quote_local_table_name
7
7
  def create_database(name, options = {})
8
- options = { encoding: 'utf8' }.merge!(options.symbolize_keys)
8
+ options = { encoding: "utf8" }.merge!(options.symbolize_keys)
9
9
 
10
- option_string = options.sum do |key, value|
10
+ option_string = options.sum("") do |key, value|
11
11
  case key
12
12
  when :owner
13
13
  " OWNER = \"#{value}\""
@@ -24,7 +24,7 @@ module Switchman
24
24
  when :connection_limit
25
25
  " CONNECTION LIMIT = #{value}"
26
26
  else
27
- ''
27
+ ""
28
28
  end
29
29
  end
30
30
 
@@ -32,12 +32,12 @@ module Switchman
32
32
  end
33
33
 
34
34
  # copy/paste; use quote_local_table_name
35
- def drop_database(name) #:nodoc:
35
+ def drop_database(name) # :nodoc:
36
36
  execute "DROP DATABASE IF EXISTS #{quote_local_table_name(name)}"
37
37
  end
38
38
 
39
39
  def current_schemas
40
- select_values('SELECT * FROM unnest(current_schemas(false))')
40
+ select_values("SELECT * FROM unnest(current_schemas(false))")
41
41
  end
42
42
 
43
43
  def extract_schema_qualified_name(string)
@@ -49,13 +49,13 @@ module Switchman
49
49
  # significant change: use the shard name if no explicit schema
50
50
  def quoted_scope(name = nil, type: nil)
51
51
  schema, name = extract_schema_qualified_name(name)
52
- type = \
52
+ type =
53
53
  case type # rubocop:disable Style/HashLikeCase
54
- when 'BASE TABLE'
54
+ when "BASE TABLE"
55
55
  "'r','p'"
56
- when 'VIEW'
56
+ when "VIEW"
57
57
  "'v','m'"
58
- when 'FOREIGN TABLE'
58
+ when "FOREIGN TABLE"
59
59
  "'f'"
60
60
  end
61
61
  scope = {}
@@ -67,7 +67,8 @@ module Switchman
67
67
 
68
68
  def foreign_keys(table_name)
69
69
  super.each do |fk|
70
- to_table_qualified_name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(fk.to_table)
70
+ to_table_qualified_name =
71
+ ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(fk.to_table)
71
72
  fk.to_table = to_table_qualified_name.identifier if to_table_qualified_name.schema == shard.name
72
73
  end
73
74
  end
@@ -101,7 +102,7 @@ module Switchman
101
102
 
102
103
  def add_index_options(_table_name, _column_name, **)
103
104
  index, algorithm, if_not_exists = super
104
- algorithm = nil if DatabaseServer.creating_new_shard && algorithm == 'CONCURRENTLY'
105
+ algorithm = nil if DatabaseServer.creating_new_shard && algorithm == "CONCURRENTLY"
105
106
  [index, algorithm, if_not_exists]
106
107
  end
107
108
 
@@ -11,11 +11,11 @@ module Switchman
11
11
  end
12
12
  end
13
13
 
14
- module AssociationQueryValue
14
+ module PolymorphicArrayValue
15
15
  def convert_to_id(value)
16
16
  case value
17
17
  when ::ActiveRecord::Base
18
- value.id
18
+ value.send(primary_key(value))
19
19
  else
20
20
  super
21
21
  end
@@ -5,28 +5,57 @@ module Switchman
5
5
  module QueryCache
6
6
  private
7
7
 
8
- def cache_sql(sql, name, binds)
9
- # have to include the shard id in the cache key because of switching dbs on the same connection
10
- sql = "#{shard.id}::#{sql}"
11
- @lock.synchronize do
12
- result =
13
- if query_cache[sql].key?(binds)
14
- args = {
15
- sql: sql,
16
- binds: binds,
17
- name: name,
18
- connection_id: object_id,
19
- cached: true,
20
- type_casted_binds: -> { type_casted_binds(binds) }
21
- }
22
- ::ActiveSupport::Notifications.instrument(
23
- 'sql.active_record',
24
- args
25
- )
26
- query_cache[sql][binds]
8
+ if ::Rails.version < "7.1"
9
+ def cache_sql(sql, name, binds)
10
+ # have to include the shard id in the cache key because of switching dbs on the same connection
11
+ sql = "#{shard.id}::#{sql}"
12
+ @lock.synchronize do
13
+ result =
14
+ if query_cache[sql].key?(binds)
15
+ args = {
16
+ sql: sql,
17
+ binds: binds,
18
+ name: name,
19
+ connection_id: object_id,
20
+ cached: true,
21
+ type_casted_binds: -> { type_casted_binds(binds) }
22
+ }
23
+ ::ActiveSupport::Notifications.instrument(
24
+ "sql.active_record",
25
+ args
26
+ )
27
+ query_cache[sql][binds]
28
+ else
29
+ query_cache[sql][binds] = yield
30
+ end
31
+ result.dup
32
+ end
33
+ end
34
+ else
35
+ def cache_sql(sql, name, binds)
36
+ # have to include the shard id in the cache key because of switching dbs on the same connection
37
+ sql = "#{shard.id}::#{sql}"
38
+ key = binds.empty? ? sql : [sql, binds]
39
+ result = nil
40
+ hit = false
41
+
42
+ @lock.synchronize do
43
+ if (result = @query_cache.delete(key))
44
+ hit = true
45
+ @query_cache[key] = result
27
46
  else
28
- query_cache[sql][binds] = yield
47
+ result = @query_cache[key] = yield
48
+ @query_cache.shift if @query_cache_max_size && @query_cache.size > @query_cache_max_size
29
49
  end
50
+ end
51
+
52
+ if hit
53
+ ::ActiveSupport::Notifications.instrument(
54
+ "sql.active_record",
55
+ cache_notification_info(sql, name, binds)
56
+ )
57
+ end
58
+
30
59
  result.dup
31
60
  end
32
61
  end