switchman 2.0.13 → 3.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 +10 -2
  3. data/app/models/switchman/shard.rb +234 -271
  4. data/app/models/switchman/unsharded_record.rb +7 -0
  5. data/db/migrate/20130328212039_create_switchman_shards.rb +1 -1
  6. data/db/migrate/20130328224244_create_default_shard.rb +5 -5
  7. data/db/migrate/20161206323434_add_back_default_string_limits_switchman.rb +1 -0
  8. data/db/migrate/20180828183945_add_default_shard_index.rb +2 -2
  9. data/db/migrate/20180828192111_add_timestamps_to_shards.rb +7 -5
  10. data/db/migrate/20190114212900_add_unique_name_indexes.rb +5 -3
  11. data/lib/switchman.rb +3 -5
  12. data/lib/switchman/action_controller/caching.rb +2 -2
  13. data/lib/switchman/active_record/abstract_adapter.rb +1 -0
  14. data/lib/switchman/active_record/association.rb +78 -89
  15. data/lib/switchman/active_record/attribute_methods.rb +58 -52
  16. data/lib/switchman/active_record/base.rb +58 -59
  17. data/lib/switchman/active_record/calculations.rb +74 -67
  18. data/lib/switchman/active_record/connection_pool.rb +14 -41
  19. data/lib/switchman/active_record/database_configurations.rb +34 -0
  20. data/lib/switchman/active_record/database_configurations/database_config.rb +13 -0
  21. data/lib/switchman/active_record/finder_methods.rb +11 -16
  22. data/lib/switchman/active_record/log_subscriber.rb +4 -8
  23. data/lib/switchman/active_record/migration.rb +6 -47
  24. data/lib/switchman/active_record/model_schema.rb +1 -1
  25. data/lib/switchman/active_record/persistence.rb +4 -6
  26. data/lib/switchman/active_record/postgresql_adapter.rb +124 -168
  27. data/lib/switchman/active_record/predicate_builder.rb +2 -2
  28. data/lib/switchman/active_record/query_cache.rb +18 -19
  29. data/lib/switchman/active_record/query_methods.rb +172 -197
  30. data/lib/switchman/active_record/reflection.rb +6 -10
  31. data/lib/switchman/active_record/relation.rb +30 -78
  32. data/lib/switchman/active_record/spawn_methods.rb +27 -29
  33. data/lib/switchman/active_record/statement_cache.rb +18 -35
  34. data/lib/switchman/active_record/tasks/database_tasks.rb +16 -0
  35. data/lib/switchman/active_support/cache.rb +3 -5
  36. data/lib/switchman/arel.rb +13 -8
  37. data/lib/switchman/database_server.rb +121 -142
  38. data/lib/switchman/default_shard.rb +52 -16
  39. data/lib/switchman/engine.rb +61 -58
  40. data/lib/switchman/environment.rb +4 -8
  41. data/lib/switchman/errors.rb +1 -0
  42. data/lib/switchman/guard_rail.rb +6 -19
  43. data/lib/switchman/guard_rail/relation.rb +5 -7
  44. data/lib/switchman/r_spec_helper.rb +29 -37
  45. data/lib/switchman/rails.rb +14 -12
  46. data/lib/switchman/schema_cache.rb +1 -9
  47. data/lib/switchman/sharded_instrumenter.rb +1 -1
  48. data/lib/switchman/standard_error.rb +15 -3
  49. data/lib/switchman/test_helper.rb +7 -11
  50. data/lib/switchman/version.rb +1 -1
  51. data/lib/tasks/switchman.rake +54 -69
  52. metadata +87 -45
  53. data/lib/switchman/active_record/batches.rb +0 -11
  54. data/lib/switchman/active_record/connection_handler.rb +0 -172
  55. data/lib/switchman/active_record/where_clause_factory.rb +0 -36
  56. data/lib/switchman/connection_pool_proxy.rb +0 -173
@@ -6,15 +6,18 @@ module Switchman
6
6
  module ActiveRecord
7
7
  module ConnectionPool
8
8
  def shard
9
- Thread.current[tls_key] || Shard.default
9
+ shard_stack.last || Shard.default
10
10
  end
11
11
 
12
- def shard=(value)
13
- Thread.current[tls_key] = value
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
14
18
  end
15
19
 
16
20
  def default_schema
17
- raise "Not postgres!" unless self.spec.config[:adapter] == 'postgresql'
18
21
  connection unless @schemas
19
22
  # default shard will not switch databases immediately, so it won't be set yet
20
23
  @schemas ||= connection.current_schemas
@@ -22,39 +25,29 @@ module Switchman
22
25
  end
23
26
 
24
27
  def checkout_new_connection
25
- conn = synchronize do
26
- # ideally I would just keep a thread-local spec that I could modify
27
- # without locking anything, but if spec returns not-the-object passed
28
- # to initialize this pool, things break
29
- spec.config[:shard_name] = self.shard.name
30
-
31
- super
32
- end
33
- conn.shard = self.shard
28
+ conn = super
29
+ conn.shard = shard
34
30
  conn
35
31
  end
36
32
 
37
33
  def connection(switch_shard: true)
38
34
  conn = super()
39
35
  raise NonExistentShardError if shard.new_record?
40
- switch_database(conn) if conn.shard != self.shard && switch_shard
36
+
37
+ switch_database(conn) if conn.shard != shard && switch_shard
41
38
  conn
42
39
  end
43
40
 
44
41
  def release_connection(with_id = Thread.current)
45
42
  super(with_id)
46
43
 
47
- if spec.config[:idle_timeout]
48
- clear_idle_connections!(Time.now - spec.config[:idle_timeout].to_i)
49
- end
44
+ flush
50
45
  end
51
46
 
52
47
  def remove_shard!(shard)
53
48
  synchronize do
54
49
  # The shard might be currently active, so we need to update our own shard
55
- if self.shard == shard
56
- self.shard = Shard.default
57
- end
50
+ self.shard = Shard.default if self.shard == shard
58
51
  # Update out any connections that may be using this shard
59
52
  @connections.each do |conn|
60
53
  # This will also update the connection's shard to the default shard
@@ -63,29 +56,9 @@ module Switchman
63
56
  end
64
57
  end
65
58
 
66
- def clear_idle_connections!(since_when)
67
- synchronize do
68
- @connections.reject! do |conn|
69
- if conn.last_query_at < since_when && !conn.in_use?
70
- conn.disconnect!
71
- true
72
- else
73
- false
74
- end
75
- end
76
- @available.clear
77
- @connections.each do |conn|
78
- @available.add conn
79
- end
80
- end
81
- end
82
-
83
59
  def switch_database(conn)
84
- if !@schemas && conn.adapter_name == 'PostgreSQL' && !self.shard.database_server.config[:shard_name]
85
- @schemas = conn.current_schemas
86
- end
60
+ @schemas = conn.current_schemas if !@schemas && conn.adapter_name == 'PostgreSQL' && !shard.database_server.config[:shard_name]
87
61
 
88
- spec.config[:shard_name] = self.shard.name
89
62
  conn.shard = shard
90
63
  end
91
64
 
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Switchman
4
+ module ActiveRecord
5
+ module DatabaseConfigurations
6
+ private
7
+
8
+ # key difference: assumes a hybrid two-tier structure; each third tier
9
+ # is implicitly named, and their config is constructing by merging into
10
+ # its parent
11
+ def build_configs(configs)
12
+ return configs.configurations if configs.is_a?(DatabaseConfigurations)
13
+ return configs if configs.is_a?(Array)
14
+
15
+ 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)
18
+
19
+ name = "#{env_name}/primary"
20
+ name = 'primary' if env_name == default_env
21
+ base_db = build_db_config_from_raw_config(env_name, name, base_config)
22
+ [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))
25
+ end
26
+ end
27
+
28
+ db_configs << environment_url_config(default_env, 'primary', {}) unless db_configs.find(&:for_current_env?)
29
+
30
+ merge_db_environment_variables(default_env, db_configs.compact)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Switchman
4
+ module ActiveRecord
5
+ module DatabaseConfigurations
6
+ module DatabaseConfig
7
+ def for_current_env?
8
+ true
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -7,19 +7,18 @@ 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.shard_category)
11
- result = self.activate do |relation, shard|
10
+ current_shard = Shard.current(klass.connection_classes)
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
14
14
  next unless current_id
15
15
  # skip the shard if the object can't be on it. unless we're only looking at one shard;
16
16
  # we might be expecting a shadow object
17
- next if current_id > Shard::IDS_PER_SHARD && self.all_shards.length > 1
17
+ next if current_id > Shard::IDS_PER_SHARD && all_shards.length > 1
18
+
18
19
  relation.call_super(:find_one, FinderMethods, current_id)
19
20
  end
20
- if result.is_a?(Array)
21
- result = result.first
22
- end
21
+ result = result.first if result.is_a?(Array)
23
22
  # we may have skipped all shards
24
23
  raise_record_not_found_exception!(id, 0, 1) unless result
25
24
  return result
@@ -34,8 +33,8 @@ module Switchman
34
33
  end
35
34
 
36
35
  def find_some_ordered(ids)
37
- current_shard = Shard.current(klass.shard_category)
38
- ids = ids.map{|id| Shard.relative_id_for(id, current_shard, current_shard)}
36
+ current_shard = Shard.current(klass.connection_classes)
37
+ ids = ids.map { |id| Shard.relative_id_for(id, current_shard, current_shard) }
39
38
  super(ids)
40
39
  end
41
40
 
@@ -45,14 +44,12 @@ module Switchman
45
44
 
46
45
  def exists?(conditions = :none)
47
46
  conditions = conditions.id if ::ActiveRecord::Base === conditions
48
- return false if !conditions
47
+ return false unless conditions
49
48
 
50
- relation = ::Rails.version >= "5.2" ?
51
- apply_join_dependency(eager_loading: false) :
52
- apply_join_dependency(self, construct_join_dependency)
49
+ relation = apply_join_dependency(eager_loading: false)
53
50
  return false if ::ActiveRecord::NullRelation === relation
54
51
 
55
- relation = relation.except(:select, :order).select("1 AS one").limit(1)
52
+ relation = relation.except(:select, :order).select('1 AS one').limit(1)
56
53
 
57
54
  case conditions
58
55
  when Array, Hash
@@ -62,9 +59,7 @@ module Switchman
62
59
  end
63
60
 
64
61
  relation.activate do |shard_rel|
65
- return true if ::Rails.version >= "5.2" ?
66
- connection.select_value(shard_rel.arel, "#{name} Exists") :
67
- connection.select_value(shard_rel, "#{name} Exists", shard_rel.bound_attributes)
62
+ return true if connection.select_value(shard_rel.arel, "#{name} Exists")
68
63
  end
69
64
  false
70
65
  end
@@ -14,20 +14,16 @@ module Switchman
14
14
 
15
15
  name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
16
16
  name = "CACHE #{name}" if payload[:cached]
17
- sql = payload[:sql].squeeze(' '.freeze)
17
+ sql = payload[:sql].squeeze(' ')
18
18
  binds = nil
19
19
  shard = payload[:shard]
20
20
  shard = " [#{shard[:database_server_id]}:#{shard[:id]} #{shard[:env]}]" if shard
21
21
 
22
22
  unless (payload[:binds] || []).empty?
23
- use_old_format = (::Rails.version < '5.1.5')
24
- args = use_old_format ?
25
- [payload[:binds], payload[:type_casted_binds]] :
26
- [payload[:type_casted_binds]]
27
- casted_params = type_casted_binds(*args)
28
- binds = " " + payload[:binds].zip(casted_params).map { |attr, value|
23
+ casted_params = type_casted_binds(payload[:type_casted_binds])
24
+ binds = ' ' + payload[:binds].zip(casted_params).map do |attr, value|
29
25
  render_bind(attr, value)
30
- }.inspect
26
+ end.inspect
31
27
  end
32
28
 
33
29
  name = colorize_payload_name(name, payload[:name])
@@ -4,73 +4,32 @@ module Switchman
4
4
  module ActiveRecord
5
5
  module Migration
6
6
  module Compatibility
7
- module V5_0
7
+ module V5_0 # rubocop:disable Naming/ClassAndModuleCamelCase
8
8
  def create_table(*args, **options)
9
- unless options.key?(:id)
10
- options[:id] = :bigserial
11
- end
12
- if block_given?
13
- super do |td|
14
- yield td
15
- end
16
- else
17
- super
18
- end
9
+ options[:id] = :bigserial unless options.key?(:id)
10
+ super
19
11
  end
20
12
  end
21
13
  end
22
14
 
23
15
  def connection
24
16
  conn = super
25
- if conn.shard != ::ActiveRecord::Base.connection_pool.current_pool.shard
26
- ::ActiveRecord::Base.connection_pool.current_pool.switch_database(conn)
27
- end
17
+ ::ActiveRecord::Base.connection_pool.switch_database(conn) if conn.shard != ::ActiveRecord::Base.connection_pool.shard
28
18
  conn
29
19
  end
30
20
  end
31
21
 
32
22
  module Migrator
33
- # significant change: hash shard id, not database name
34
23
  def generate_migrator_advisory_lock_id
35
- shard_name_hash = Zlib.crc32(Shard.current.name)
24
+ shard_name_hash = Zlib.crc32("#{Shard.current.id}:#{Shard.current.name}")
36
25
  ::ActiveRecord::Migrator::MIGRATOR_SALT * shard_name_hash
37
26
  end
38
-
39
- if ::Rails.version >= '6.0'
40
- # copy/paste from Rails 6.1
41
- def with_advisory_lock
42
- lock_id = generate_migrator_advisory_lock_id
43
-
44
- with_advisory_lock_connection do |connection|
45
- got_lock = connection.get_advisory_lock(lock_id)
46
- raise ::ActiveRecord::ConcurrentMigrationError unless got_lock
47
- load_migrated # reload schema_migrations to be sure it wasn't changed by another process before we got the lock
48
- yield
49
- ensure
50
- if got_lock && !connection.release_advisory_lock(lock_id)
51
- raise ::ActiveRecord::ConcurrentMigrationError.new(
52
- ::ActiveRecord::ConcurrentMigrationError::RELEASE_LOCK_FAILED_MESSAGE
53
- )
54
- end
55
- end
56
- end
57
-
58
- # significant change: strip out prefer_secondary from config
59
- def with_advisory_lock_connection
60
- pool = ::ActiveRecord::ConnectionAdapters::ConnectionHandler.new.establish_connection(
61
- ::ActiveRecord::Base.connection_config.except(:prefer_secondary)
62
- )
63
-
64
- pool.with_connection { |connection| yield(connection) }
65
- ensure
66
- pool&.disconnect!
67
- end
68
- end
69
27
  end
70
28
 
71
29
  module MigrationContext
72
30
  def migrations
73
31
  return @migrations if instance_variable_defined?(:@migrations)
32
+
74
33
  migrations_cache = Thread.current[:migrations_cache] ||= {}
75
34
  key = Digest::MD5.hexdigest(migration_files.sort.join(','))
76
35
  @migrations = migrations_cache[key] ||= super
@@ -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(shard_category).id] ||= connection.quote_table_name(table_name)
9
+ @quoted_table_name[Shard.current(connection_classes).id] ||= connection.quote_table_name(table_name)
10
10
  end
11
11
  end
12
12
  end
@@ -5,14 +5,12 @@ 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.shard_category) { super }
8
+ shard.activate(self.class.connection_classes) { super }
9
9
  end
10
10
 
11
- if ::Rails.version >= '5.2'
12
- def update_columns(*)
13
- shard.activate(self.class.shard_category) { super }
14
- end
11
+ def update_columns(*)
12
+ shard.activate(self.class.connection_classes) { super }
15
13
  end
16
14
  end
17
15
  end
18
- end
16
+ end
@@ -9,22 +9,22 @@ module Switchman
9
9
 
10
10
  option_string = options.sum do |key, value|
11
11
  case key
12
- when :owner
13
- " OWNER = \"#{value}\""
14
- when :template
15
- " TEMPLATE = \"#{value}\""
16
- when :encoding
17
- " ENCODING = '#{value}'"
18
- when :collation
19
- " LC_COLLATE = '#{value}'"
20
- when :ctype
21
- " LC_CTYPE = '#{value}'"
22
- when :tablespace
23
- " TABLESPACE = \"#{value}\""
24
- when :connection_limit
25
- " CONNECTION LIMIT = #{value}"
26
- else
27
- ""
12
+ when :owner
13
+ " OWNER = \"#{value}\""
14
+ when :template
15
+ " TEMPLATE = \"#{value}\""
16
+ when :encoding
17
+ " ENCODING = '#{value}'"
18
+ when :collation
19
+ " LC_COLLATE = '#{value}'"
20
+ when :ctype
21
+ " LC_CTYPE = '#{value}'"
22
+ when :tablespace
23
+ " TABLESPACE = \"#{value}\""
24
+ when :connection_limit
25
+ " CONNECTION LIMIT = #{value}"
26
+ else
27
+ ''
28
28
  end
29
29
  end
30
30
 
@@ -37,166 +37,92 @@ module Switchman
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
+ end
42
+
43
+ def tables(_name = nil)
44
+ query(<<-SQL, 'SCHEMA').map { |row| row[0] }
45
+ SELECT tablename
46
+ FROM pg_tables
47
+ WHERE schemaname = '#{shard.name}'
48
+ SQL
41
49
  end
42
50
 
43
51
  def extract_schema_qualified_name(string)
44
52
  name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(string.to_s)
45
- if string && !name.schema
46
- name.instance_variable_set(:@schema, shard.name)
47
- end
53
+ name.instance_variable_set(:@schema, shard.name) if string && !name.schema
48
54
  [name.schema, name.identifier]
49
55
  end
50
56
 
51
- # significant change: use the shard name if no explicit schema
52
- def quoted_scope(name = nil, type: nil)
53
- schema, name = extract_schema_qualified_name(name)
54
- type = \
55
- case type
56
- when "BASE TABLE"
57
- "'r','p'"
58
- when "VIEW"
59
- "'v','m'"
60
- when "FOREIGN TABLE"
61
- "'f'"
62
- end
63
- scope = {}
64
- scope[:schema] = quote(schema || shard.name)
65
- scope[:name] = quote(name) if name
66
- scope[:type] = type if type
67
- scope
57
+ def view_exists?(name)
58
+ name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(name.to_s)
59
+ return false unless name.identifier
60
+
61
+ name.instance_variable_set(:@schema, shard.name) unless name.schema
62
+
63
+ select_values(<<-SQL, 'SCHEMA').any?
64
+ SELECT c.relname
65
+ FROM pg_class c
66
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
67
+ WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view
68
+ AND c.relname = '#{name.identifier}'
69
+ AND n.nspname = '#{shard.name}'
70
+ SQL
68
71
  end
69
72
 
70
- if ::Rails.version < '6.0'
71
- def tables(name = nil)
72
- query(<<-SQL, 'SCHEMA').map { |row| row[0] }
73
- SELECT tablename
74
- FROM pg_tables
75
- WHERE schemaname = '#{shard.name}'
73
+ def indexes(table_name)
74
+ result = query(<<-SQL, 'SCHEMA')
75
+ SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid
76
+ FROM pg_class t
77
+ INNER JOIN pg_index d ON t.oid = d.indrelid
78
+ INNER JOIN pg_class i ON d.indexrelid = i.oid
79
+ WHERE i.relkind = 'i'
80
+ AND d.indisprimary = 'f'
81
+ AND t.relname = '#{table_name}'
82
+ AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = '#{shard.name}' )
83
+ ORDER BY i.relname
84
+ SQL
85
+
86
+ result.map do |row|
87
+ index_name = row[0]
88
+ unique = row[1] == true || row[1] == 't'
89
+ indkey = row[2].split
90
+ inddef = row[3]
91
+ oid = row[4]
92
+
93
+ columns = Hash[query(<<-SQL, 'SCHEMA')] # rubocop:disable Style/HashConversion
94
+ SELECT a.attnum, a.attname
95
+ FROM pg_attribute a
96
+ WHERE a.attrelid = #{oid}
97
+ AND a.attnum IN (#{indkey.join(',')})
76
98
  SQL
77
- end
78
99
 
79
- def view_exists?(name)
80
- name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(name.to_s)
81
- return false unless name.identifier
82
- if !name.schema
83
- name.instance_variable_set(:@schema, shard.name)
84
- end
100
+ column_names = columns.stringify_keys.values_at(*indkey).compact
85
101
 
86
- select_values(<<-SQL, 'SCHEMA').any?
87
- SELECT c.relname
88
- FROM pg_class c
89
- LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
90
- WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view
91
- AND c.relname = '#{name.identifier}'
92
- AND n.nspname = '#{shard.name}'
93
- SQL
94
- end
102
+ next if column_names.empty?
95
103
 
96
- def indexes(table_name)
97
- result = query(<<-SQL, 'SCHEMA')
98
- SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid
104
+ # add info on sort order for columns (only desc order is explicitly specified, asc is the default)
105
+ desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
106
+ orders = desc_order_columns.any? ? Hash[desc_order_columns.map { |order_column| [order_column, :desc] }] : {} # rubocop:disable Style/HashConversion
107
+ where = inddef.scan(/WHERE (.+)$/).flatten[0]
108
+ using = inddef.scan(/USING (.+?) /).flatten[0].to_sym
109
+
110
+ ::ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, index_name, unique, column_names,
111
+ orders: orders, where: where, using: using)
112
+ end.compact
113
+ end
114
+
115
+ def index_name_exists?(table_name, index_name, _default = nil)
116
+ exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i.positive?
117
+ SELECT COUNT(*)
99
118
  FROM pg_class t
100
119
  INNER JOIN pg_index d ON t.oid = d.indrelid
101
120
  INNER JOIN pg_class i ON d.indexrelid = i.oid
102
121
  WHERE i.relkind = 'i'
103
- AND d.indisprimary = 'f'
122
+ AND i.relname = '#{index_name}'
104
123
  AND t.relname = '#{table_name}'
105
124
  AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = '#{shard.name}' )
106
- ORDER BY i.relname
107
- SQL
108
-
109
-
110
- result.map do |row|
111
- index_name = row[0]
112
- unique = row[1] == true || row[1] == 't'
113
- indkey = row[2].split(" ")
114
- inddef = row[3]
115
- oid = row[4]
116
-
117
- columns = Hash[query(<<-SQL, "SCHEMA")]
118
- SELECT a.attnum, a.attname
119
- FROM pg_attribute a
120
- WHERE a.attrelid = #{oid}
121
- AND a.attnum IN (#{indkey.join(",")})
122
- SQL
123
-
124
- column_names = columns.stringify_keys.values_at(*indkey).compact
125
-
126
- unless column_names.empty?
127
- # add info on sort order for columns (only desc order is explicitly specified, asc is the default)
128
- desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
129
- orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {}
130
- where = inddef.scan(/WHERE (.+)$/).flatten[0]
131
- using = inddef.scan(/USING (.+?) /).flatten[0].to_sym
132
-
133
- if ::Rails.version >= "5.2"
134
- ::ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, index_name, unique, column_names, orders: orders, where: where, using: using)
135
- else
136
- ::ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using)
137
- end
138
- end
139
- end.compact
140
- end
141
-
142
- def index_name_exists?(table_name, index_name, _default = nil)
143
- exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
144
- SELECT COUNT(*)
145
- FROM pg_class t
146
- INNER JOIN pg_index d ON t.oid = d.indrelid
147
- INNER JOIN pg_class i ON d.indexrelid = i.oid
148
- WHERE i.relkind = 'i'
149
- AND i.relname = '#{index_name}'
150
- AND t.relname = '#{table_name}'
151
- AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = '#{shard.name}' )
152
- SQL
153
- end
154
-
155
- def foreign_keys(table_name)
156
- # mostly copy-pasted from AR - only change is to the nspname condition for qualified names support
157
- fk_info = select_all <<-SQL.strip_heredoc
158
- SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete
159
- FROM pg_constraint c
160
- JOIN pg_class t1 ON c.conrelid = t1.oid
161
- JOIN pg_class t2 ON c.confrelid = t2.oid
162
- JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
163
- JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
164
- JOIN pg_namespace t3 ON c.connamespace = t3.oid
165
- WHERE c.contype = 'f'
166
- AND t1.relname = #{quote(table_name)}
167
- AND t3.nspname = '#{shard.name}'
168
- ORDER BY c.conname
169
- SQL
170
-
171
- fk_info.map do |row|
172
- options = {
173
- column: row['column'],
174
- name: row['name'],
175
- primary_key: row['primary_key']
176
- }
177
-
178
- options[:on_delete] = extract_foreign_key_action(row['on_delete'])
179
- options[:on_update] = extract_foreign_key_action(row['on_update'])
180
-
181
- # strip the schema name from to_table if it matches
182
- to_table = row['to_table']
183
- to_table_qualified_name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(to_table)
184
- if to_table_qualified_name.schema == shard.name
185
- to_table = to_table_qualified_name.identifier
186
- end
187
-
188
- ::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(table_name, to_table, options)
189
- end
190
- end
191
- else
192
- def foreign_keys(table_name)
193
- super.each do |fk|
194
- to_table_qualified_name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(fk.to_table)
195
- if to_table_qualified_name.schema == shard.name
196
- fk.to_table = to_table_qualified_name.identifier
197
- end
198
- end
199
- end
125
+ SQL
200
126
  end
201
127
 
202
128
  def quote_local_table_name(name)
@@ -208,18 +134,13 @@ module Switchman
208
134
 
209
135
  def quote_table_name(name)
210
136
  return quote_local_table_name(name) if @use_local_table_name
137
+
211
138
  name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(name.to_s)
212
- if !name.schema
213
- name.instance_variable_set(:@schema, shard.name)
214
- end
139
+ name.instance_variable_set(:@schema, shard.name) unless name.schema
215
140
  name.quoted
216
141
  end
217
142
 
218
- def with_global_table_name(&block)
219
- with_local_table_name(false, &block)
220
- end
221
-
222
- def with_local_table_name(enable = true)
143
+ def with_local_table_name(enable = true) # rubocop:disable Style/OptionalBooleanParameter
223
144
  old_value = @use_local_table_name
224
145
  @use_local_table_name = enable
225
146
  yield
@@ -227,10 +148,45 @@ module Switchman
227
148
  @use_local_table_name = old_value
228
149
  end
229
150
 
151
+ def foreign_keys(table_name)
152
+ # mostly copy-pasted from AR - only change is to the nspname condition for qualified names support
153
+ fk_info = select_all <<-SQL.strip_heredoc
154
+ SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete
155
+ FROM pg_constraint c
156
+ JOIN pg_class t1 ON c.conrelid = t1.oid
157
+ JOIN pg_class t2 ON c.confrelid = t2.oid
158
+ JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
159
+ JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
160
+ JOIN pg_namespace t3 ON c.connamespace = t3.oid
161
+ WHERE c.contype = 'f'
162
+ AND t1.relname = #{quote(table_name)}
163
+ AND t3.nspname = '#{shard.name}'
164
+ ORDER BY c.conname
165
+ SQL
166
+
167
+ fk_info.map do |row|
168
+ options = {
169
+ column: row['column'],
170
+ name: row['name'],
171
+ primary_key: row['primary_key']
172
+ }
173
+
174
+ options[:on_delete] = extract_foreign_key_action(row['on_delete'])
175
+ options[:on_update] = extract_foreign_key_action(row['on_update'])
176
+
177
+ # strip the schema name from to_table if it matches
178
+ to_table = row['to_table']
179
+ to_table_qualified_name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(to_table)
180
+ to_table = to_table_qualified_name.identifier if to_table_qualified_name.schema == shard.name
181
+
182
+ ::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(table_name, to_table, options)
183
+ end
184
+ end
185
+
230
186
  def add_index_options(_table_name, _column_name, **)
231
- index_name, index_type, index_columns, index_options, algorithm, using = super
232
- algorithm = nil if DatabaseServer.creating_new_shard && algorithm == "CONCURRENTLY"
233
- [index_name, index_type, index_columns, index_options, algorithm, using]
187
+ index, algorithm, if_not_exists = super
188
+ algorithm = nil if DatabaseServer.creating_new_shard && algorithm == 'CONCURRENTLY'
189
+ [index, algorithm, if_not_exists]
234
190
  end
235
191
 
236
192
  def rename_table(table_name, new_name)