switchman 2.0.13 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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)