switchman 2.2.3 → 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 +272 -275
  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/action_controller/caching.rb +2 -2
  12. data/lib/switchman/active_record/abstract_adapter.rb +1 -0
  13. data/lib/switchman/active_record/association.rb +78 -89
  14. data/lib/switchman/active_record/attribute_methods.rb +58 -73
  15. data/lib/switchman/active_record/base.rb +59 -60
  16. data/lib/switchman/active_record/calculations.rb +74 -67
  17. data/lib/switchman/active_record/connection_pool.rb +14 -43
  18. data/lib/switchman/active_record/database_configurations/database_config.rb +13 -0
  19. data/lib/switchman/active_record/database_configurations.rb +34 -0
  20. data/lib/switchman/active_record/finder_methods.rb +11 -16
  21. data/lib/switchman/active_record/log_subscriber.rb +4 -5
  22. data/lib/switchman/active_record/migration.rb +7 -51
  23. data/lib/switchman/active_record/model_schema.rb +1 -1
  24. data/lib/switchman/active_record/persistence.rb +3 -14
  25. data/lib/switchman/active_record/postgresql_adapter.rb +125 -169
  26. data/lib/switchman/active_record/predicate_builder.rb +2 -2
  27. data/lib/switchman/active_record/query_cache.rb +18 -19
  28. data/lib/switchman/active_record/query_methods.rb +168 -216
  29. data/lib/switchman/active_record/reflection.rb +7 -22
  30. data/lib/switchman/active_record/relation.rb +30 -78
  31. data/lib/switchman/active_record/spawn_methods.rb +27 -29
  32. data/lib/switchman/active_record/statement_cache.rb +18 -35
  33. data/lib/switchman/active_record/tasks/database_tasks.rb +16 -0
  34. data/lib/switchman/active_support/cache.rb +3 -5
  35. data/lib/switchman/arel.rb +13 -8
  36. data/lib/switchman/database_server.rb +121 -142
  37. data/lib/switchman/default_shard.rb +52 -16
  38. data/lib/switchman/engine.rb +62 -59
  39. data/lib/switchman/environment.rb +4 -8
  40. data/lib/switchman/errors.rb +1 -0
  41. data/lib/switchman/guard_rail/relation.rb +5 -7
  42. data/lib/switchman/guard_rail.rb +6 -19
  43. data/lib/switchman/r_spec_helper.rb +29 -37
  44. data/lib/switchman/rails.rb +14 -12
  45. data/lib/switchman/schema_cache.rb +1 -9
  46. data/lib/switchman/sharded_instrumenter.rb +1 -1
  47. data/lib/switchman/standard_error.rb +15 -3
  48. data/lib/switchman/test_helper.rb +6 -4
  49. data/lib/switchman/version.rb +1 -1
  50. data/lib/switchman.rb +3 -5
  51. data/lib/tasks/switchman.rake +55 -71
  52. metadata +88 -46
  53. data/lib/switchman/active_record/batches.rb +0 -11
  54. data/lib/switchman/active_record/connection_handler.rb +0 -190
  55. data/lib/switchman/active_record/where_clause_factory.rb +0 -36
  56. data/lib/switchman/connection_pool_proxy.rb +0 -173
@@ -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,17 +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
- args = [payload[:type_casted_binds]]
24
- casted_params = type_casted_binds(*args)
25
- 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|
26
25
  render_bind(attr, value)
27
- }.inspect
26
+ end.inspect
28
27
  end
29
28
 
30
29
  name = colorize_payload_name(name, payload[:name])
@@ -4,76 +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: just return MIGRATOR_SALT directly
34
- # especially if you're going through pgbouncer, the database
35
- # name you're accessing may not be consistent. it is NOT allowed
36
- # to run migrations against multiple shards in the same database
37
- # concurrently
38
23
  def generate_migrator_advisory_lock_id
39
- ::ActiveRecord::Migrator::MIGRATOR_SALT
40
- end
41
-
42
- if ::Rails.version >= '6.0'
43
- # copy/paste from Rails 6.1
44
- def with_advisory_lock
45
- lock_id = generate_migrator_advisory_lock_id
46
-
47
- with_advisory_lock_connection do |connection|
48
- got_lock = connection.get_advisory_lock(lock_id)
49
- raise ::ActiveRecord::ConcurrentMigrationError unless got_lock
50
- load_migrated # reload schema_migrations to be sure it wasn't changed by another process before we got the lock
51
- yield
52
- ensure
53
- if got_lock && !connection.release_advisory_lock(lock_id)
54
- raise ::ActiveRecord::ConcurrentMigrationError.new(
55
- ::ActiveRecord::ConcurrentMigrationError::RELEASE_LOCK_FAILED_MESSAGE
56
- )
57
- end
58
- end
59
- end
60
-
61
- # significant change: strip out prefer_secondary from config
62
- def with_advisory_lock_connection
63
- pool = ::ActiveRecord::ConnectionAdapters::ConnectionHandler.new.establish_connection(
64
- ::ActiveRecord::Base.connection_config.except(:prefer_secondary)
65
- )
66
-
67
- pool.with_connection { |connection| yield(connection) }
68
- ensure
69
- pool&.disconnect!
70
- end
24
+ shard_name_hash = Zlib.crc32("#{Shard.current.id}:#{Shard.current.name}")
25
+ ::ActiveRecord::Migrator::MIGRATOR_SALT * shard_name_hash
71
26
  end
72
27
  end
73
28
 
74
29
  module MigrationContext
75
30
  def migrations
76
31
  return @migrations if instance_variable_defined?(:@migrations)
32
+
77
33
  migrations_cache = Thread.current[:migrations_cache] ||= {}
78
34
  key = Digest::MD5.hexdigest(migration_files.sort.join(','))
79
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,22 +5,11 @@ 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
15
- end
16
-
17
- def delete
18
- db = shard.database_server
19
- if ::GuardRail.environment != db.guard_rail_environment
20
- return db.unguard { super }
21
- else
22
- super
23
- end
11
+ def update_columns(*)
12
+ shard.activate(self.class.connection_classes) { super }
24
13
  end
25
14
  end
26
15
  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
- AND t.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
124
+ AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = '#{shard.name}' )
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)
@@ -15,7 +15,7 @@ module Switchman
15
15
  def convert_to_id(value)
16
16
  case value
17
17
  when ::ActiveRecord::Base
18
- value.id
18
+ value.send(primary_key)
19
19
  else
20
20
  super
21
21
  end
@@ -23,4 +23,4 @@ module Switchman
23
23
  end
24
24
  end
25
25
  end
26
- end
26
+ end
@@ -3,31 +3,30 @@
3
3
  module Switchman
4
4
  module ActiveRecord
5
5
  module QueryCache
6
-
7
6
  private
8
7
 
9
8
  def cache_sql(sql, name, binds)
10
9
  # have to include the shard id in the cache key because of switching dbs on the same connection
11
- sql = "#{self.shard.id}::#{sql}"
10
+ sql = "#{shard.id}::#{sql}"
12
11
  @lock.synchronize do
13
12
  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
- }
22
- args[:type_casted_binds] = -> { type_casted_binds(binds) }
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
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]
27
+ else
28
+ query_cache[sql][binds] = yield
29
+ end
31
30
  result.dup
32
31
  end
33
32
  end