switchman 3.0.1 → 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 +93 -50
  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 +252 -135
  25. data/lib/switchman/active_record/reflection.rb +10 -3
  26. data/lib/switchman/active_record/relation.rb +154 -32
  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 +46 -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,71 +1,84 @@
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
11
-
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)
16
- end
17
- shard_stack
18
- end
19
-
20
6
  def default_schema
21
- connection unless @schemas
7
+ connection_method = (::Rails.version < "7.2") ? :connection : :lease_connection
8
+ send(connection_method) unless @schemas
22
9
  # default shard will not switch databases immediately, so it won't be set yet
23
- @schemas ||= connection.current_schemas
10
+ @schemas ||= send(connection_method).current_schemas
24
11
  @schemas.first
25
12
  end
26
13
 
27
14
  def checkout_new_connection
28
15
  conn = super
29
- conn.shard = shard
16
+ conn.shard = current_shard
30
17
  conn
31
18
  end
32
19
 
33
20
  def connection(switch_shard: true)
34
21
  conn = super()
35
- raise NonExistentShardError if shard.new_record?
22
+ raise Errors::NonExistentShardError if current_shard.new_record?
36
23
 
37
- switch_database(conn) if conn.shard != shard && switch_shard
24
+ switch_database(conn) if conn.shard != current_shard && switch_shard
38
25
  conn
39
26
  end
40
27
 
41
- def release_connection(with_id = Thread.current)
42
- super(with_id)
28
+ unless ::Rails.version < "7.2"
29
+ def active_connection(switch_shard: true)
30
+ conn = super()
31
+ return nil if conn.nil?
32
+ raise Errors::NonExistentShardError if current_shard.new_record?
43
33
 
44
- flush
45
- end
34
+ switch_database(conn) if conn.shard != current_shard && switch_shard
35
+ conn
36
+ end
37
+
38
+ def lease_connection(switch_shard: true)
39
+ conn = super()
40
+ raise Errors::NonExistentShardError if current_shard.new_record?
46
41
 
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
42
+ switch_database(conn) if conn.shard != current_shard && switch_shard
43
+ conn
44
+ end
45
+
46
+ def with_connection(switch_shard: true, **kwargs)
47
+ super(**kwargs) do |conn|
48
+ raise Errors::NonExistentShardError if current_shard.new_record?
49
+
50
+ switch_database(conn) if conn.shard != current_shard && switch_shard
51
+ yield conn
55
52
  end
56
53
  end
57
54
  end
58
55
 
56
+ def release_connection(with_id = Thread.current)
57
+ super
58
+
59
+ flush
60
+ end
61
+
59
62
  def switch_database(conn)
60
- @schemas = conn.current_schemas if !@schemas && conn.adapter_name == 'PostgreSQL' && !shard.database_server.config[:shard_name]
63
+ if !@schemas && conn.adapter_name == "PostgreSQL" && !current_shard.database_server.config[:shard_name]
64
+ @schemas = conn.current_schemas
65
+ end
61
66
 
62
- conn.shard = shard
67
+ conn.shard = current_shard
63
68
  end
64
69
 
65
70
  private
66
71
 
72
+ def current_shard
73
+ if ::Rails.version < "8.0"
74
+ connection_class.current_switchman_shard
75
+ else
76
+ connection_descriptor.name.constantize.current_switchman_shard
77
+ end
78
+ end
79
+
67
80
  def tls_key
68
- "#{object_id}_shard".to_sym
81
+ :"#{object_id}_shard"
69
82
  end
70
83
  end
71
84
  end
@@ -3,6 +3,20 @@
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
+ def configs_for(include_hidden: false, name: nil, **)
11
+ res = super
12
+ if name && !include_hidden
13
+ return nil unless name.end_with?("primary")
14
+ elsif !include_hidden
15
+ return res.select { |config| config.name.end_with?("primary") }
16
+ end
17
+ res
18
+ end
19
+
6
20
  private
7
21
 
8
22
  # key difference: assumes a hybrid two-tier structure; each third tier
@@ -13,19 +27,31 @@ module Switchman
13
27
  return configs if configs.is_a?(Array)
14
28
 
15
29
  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)
30
+ if config.is_a?(Hash)
31
+ # It would be nice to do the auto-fallback that we want here, but we haven't
32
+ # actually done that for years (or maybe ever) and it will be a big lift to get working
33
+ roles = config.keys.select do |k|
34
+ config[k].is_a?(Hash) || (config[k].is_a?(Array) && config[k].all?(Hash))
35
+ end
36
+ base_config = config.except(*roles)
37
+ else
38
+ base_config = config
39
+ roles = []
40
+ end
18
41
 
19
42
  name = "#{env_name}/primary"
20
- name = 'primary' if env_name == default_env
43
+ name = "primary" if env_name == default_env
21
44
  base_db = build_db_config_from_raw_config(env_name, name, base_config)
22
45
  [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))
46
+ build_db_config_from_raw_config(
47
+ env_name,
48
+ "#{env_name}/#{role}",
49
+ base_config.merge(config[role].is_a?(Array) ? config[role].first : config[role])
50
+ )
25
51
  end
26
52
  end
27
53
 
28
- db_configs << environment_url_config(default_env, 'primary', {}) unless db_configs.find(&:for_current_env?)
54
+ db_configs << environment_url_config(default_env, "primary", {}) unless db_configs.find(&:for_current_env?)
29
55
 
30
56
  merge_db_environment_variables(default_env, db_configs.compact)
31
57
  end
@@ -4,10 +4,10 @@ module Switchman
4
4
  module ActiveRecord
5
5
  module FinderMethods
6
6
  def find_one(id)
7
- return super(id) unless klass.integral_id?
7
+ return super 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
@@ -28,14 +28,14 @@ module Switchman
28
28
  if shard
29
29
  shard.activate { super(local_id) }
30
30
  else
31
- super(id)
31
+ super
32
32
  end
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
- super(ids)
38
+ super
39
39
  end
40
40
 
41
41
  def find_or_instantiator_by_attributes(match, attributes, *args)
@@ -43,23 +43,29 @@ module Switchman
43
43
  end
44
44
 
45
45
  def exists?(conditions = :none)
46
- conditions = conditions.id if ::ActiveRecord::Base === conditions
47
- return false unless conditions
46
+ return false if @none
48
47
 
49
- relation = apply_join_dependency(eager_loading: false)
50
- return false if ::ActiveRecord::NullRelation === relation
48
+ if Base === conditions
49
+ raise ArgumentError, <<-TEXT.squish
50
+ You are passing an instance of ActiveRecord::Base to `exists?`.
51
+ Please pass the id of the object by calling `.id`.
52
+ TEXT
53
+ end
51
54
 
52
- relation = relation.except(:select, :order).select('1 AS one').limit(1)
55
+ return false if !conditions || limit_value == 0 # rubocop:disable Style/NumericPredicate
53
56
 
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
57
+ if eager_loading?
58
+ relation = apply_join_dependency(eager_loading: false)
59
+ return relation.exists?(conditions)
59
60
  end
60
61
 
62
+ relation = construct_relation_for_exists(conditions)
63
+ return false if relation.where_clause.contradiction?
64
+
61
65
  relation.activate do |shard_rel|
62
- return true if connection.select_value(shard_rel.arel, "#{name} Exists")
66
+ return true if skip_query_cache_if_necessary do
67
+ connection.select_rows(shard_rel.arel, "#{name} Exists?").size == 1
68
+ end
63
69
  end
64
70
  false
65
71
  end
@@ -5,29 +5,26 @@ 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?
10
-
11
8
  payload = event.payload
12
9
 
13
10
  return if ::ActiveRecord::LogSubscriber::IGNORE_PAYLOAD_NAMES.include?(payload[:name])
14
11
 
15
12
  name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
16
13
  name = "CACHE #{name}" if payload[:cached]
17
- sql = payload[:sql].squeeze(' ')
14
+ sql = payload[:sql].squeeze(" ")
18
15
  binds = nil
19
16
  shard = payload[:shard]
20
17
  shard = " [#{shard[:database_server_id]}:#{shard[:id]} #{shard[:env]}]" if shard
21
18
 
22
19
  unless (payload[:binds] || []).empty?
23
20
  casted_params = type_casted_binds(payload[:type_casted_binds])
24
- binds = ' ' + payload[:binds].zip(casted_params).map do |attr, value|
21
+ binds = " " + payload[:binds].zip(casted_params).map do |attr, value|
25
22
  render_bind(attr, value)
26
23
  end.inspect
27
24
  end
28
25
 
29
26
  name = colorize_payload_name(name, payload[:name])
30
- sql = color(sql, sql_color(sql), true)
27
+ sql = color(sql, sql_color(sql), bold: true)
31
28
 
32
29
  debug " #{name} #{sql}#{binds}#{shard}"
33
30
  end
@@ -14,38 +14,66 @@ 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
+ @internal_metadata[:migrator_advisory_lock_id] = shard_name_hash.to_s
33
+ shard_name_hash
27
34
  end
28
35
 
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!
36
+ def use_advisory_lock?
37
+ super && pending_migrations.any?
38
38
  end
39
39
  end
40
40
 
41
41
  module MigrationContext
42
+ def migrate(...)
43
+ schema_cache_holder = ::ActiveRecord::Base.connection_pool
44
+ schema_cache_holder = schema_cache_holder.schema_reflection
45
+ previous_schema_cache = schema_cache_holder.instance_variable_get(:@cache)
46
+
47
+ schema_cache_holder.instance_variable_get(:@cache)
48
+
49
+ reset_column_information
50
+ schema_cache_holder.clear!
51
+
52
+ begin
53
+ super
54
+ ensure
55
+ if ::Rails.version < "7.2"
56
+ schema_cache_holder.set_schema_cache(previous_schema_cache)
57
+ else
58
+ schema_cache_holder.instance_variable_set(:@cache, previous_schema_cache)
59
+ end
60
+ reset_column_information
61
+ end
62
+ end
63
+
42
64
  def migrations
43
65
  return @migrations if instance_variable_defined?(:@migrations)
44
66
 
45
67
  migrations_cache = Thread.current[:migrations_cache] ||= {}
46
- key = Digest::MD5.hexdigest(migration_files.sort.join(','))
68
+ key = Digest::MD5.hexdigest(migration_files.sort.join(","))
47
69
  @migrations = migrations_cache[key] ||= super
48
70
  end
71
+
72
+ private
73
+
74
+ def reset_column_information
75
+ ::ActiveRecord::Base.descendants.reject { |m| m <= UnshardedRecord }.each(&:reset_column_information)
76
+ end
49
77
  end
50
78
  end
51
79
  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(**, &)
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,24 +67,43 @@ 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
74
75
 
76
+ module ClassMethods
77
+ def quote_local_table_name(name)
78
+ # postgres quotes tables and columns the same; just pass through
79
+ # (differs from quote_table_name_with_shard below by no logic to
80
+ # explicitly qualify the table)
81
+ quote_column_name(name)
82
+ end
83
+
84
+ def quote_table_name(name, shard: nil)
85
+ # This looks kind of weird at first glance, but older Rails versions do not actually import
86
+ # these methods as class methods.
87
+ shard = self.shard if shard.nil? && ::Rails.version < "7.2" && !@use_local_table_name
88
+
89
+ return quote_local_table_name(name) unless shard
90
+
91
+ name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(name.to_s)
92
+ name.instance_variable_set(:@schema, shard.name) unless name.schema
93
+ name.quoted
94
+ end
95
+ end
96
+
75
97
  def quote_local_table_name(name)
76
- # postgres quotes tables and columns the same; just pass through
77
- # (differs from quote_table_name_with_shard below by no logic to
78
- # explicitly qualify the table)
79
- quote_column_name(name)
98
+ self.class.quote_local_table_name(name)
80
99
  end
81
100
 
82
101
  def quote_table_name(name)
83
- return quote_local_table_name(name) if @use_local_table_name
102
+ self.class.quote_table_name(name, shard: @use_local_table_name ? nil : shard)
103
+ end
84
104
 
85
- name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(name.to_s)
86
- name.instance_variable_set(:@schema, shard.name) unless name.schema
87
- name.quoted
105
+ def with_global_table_name(&)
106
+ with_local_table_name(false, &)
88
107
  end
89
108
 
90
109
  def with_local_table_name(enable = true) # rubocop:disable Style/OptionalBooleanParameter
@@ -97,7 +116,7 @@ module Switchman
97
116
 
98
117
  def add_index_options(_table_name, _column_name, **)
99
118
  index, algorithm, if_not_exists = super
100
- algorithm = nil if DatabaseServer.creating_new_shard && algorithm == 'CONCURRENTLY'
119
+ algorithm = nil if DatabaseServer.creating_new_shard && algorithm == "CONCURRENTLY"
101
120
  [index, algorithm, if_not_exists]
102
121
  end
103
122
 
@@ -124,7 +143,7 @@ module Switchman
124
143
  end
125
144
 
126
145
  def columns(*)
127
- with_local_table_name(false) { super }
146
+ with_global_table_name { super }
128
147
  end
129
148
  end
130
149
  end
@@ -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.send(primary_key)
18
+ value.send(primary_key(value))
19
19
  else
20
20
  super
21
21
  end
@@ -8,27 +8,36 @@ module Switchman
8
8
  def cache_sql(sql, name, binds)
9
9
  # have to include the shard id in the cache key because of switching dbs on the same connection
10
10
  sql = "#{shard.id}::#{sql}"
11
+ key = binds.empty? ? sql : [sql, binds]
12
+ result = nil
13
+ hit = false
14
+
11
15
  @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]
16
+ if ::Rails.version < "7.2"
17
+ if (result = @query_cache.delete(key))
18
+ hit = true
19
+ @query_cache[key] = result
27
20
  else
28
- query_cache[sql][binds] = yield
21
+ result = @query_cache[key] = yield
22
+ @query_cache.shift if @query_cache_max_size && @query_cache.size > @query_cache_max_size
23
+ end
24
+ else
25
+ hit = true
26
+ result = @query_cache.compute_if_absent(key) do
27
+ hit = false
28
+ yield
29
29
  end
30
- result.dup
30
+ end
31
31
  end
32
+
33
+ if hit
34
+ ::ActiveSupport::Notifications.instrument(
35
+ "sql.active_record",
36
+ cache_notification_info(sql, name, binds)
37
+ )
38
+ end
39
+
40
+ result.dup
32
41
  end
33
42
  end
34
43
  end