switchman 1.5.21 → 2.1.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 +5 -5
  2. data/app/models/switchman/shard.rb +757 -11
  3. data/db/migrate/20130328212039_create_switchman_shards.rb +3 -1
  4. data/db/migrate/20130328224244_create_default_shard.rb +4 -2
  5. data/db/migrate/20161206323434_add_back_default_string_limits_switchman.rb +13 -0
  6. data/db/migrate/20180828183945_add_default_shard_index.rb +15 -0
  7. data/db/migrate/20180828192111_add_timestamps_to_shards.rb +17 -0
  8. data/db/migrate/20190114212900_add_unique_name_indexes.rb +9 -0
  9. data/lib/switchman/action_controller/caching.rb +2 -0
  10. data/lib/switchman/active_record/abstract_adapter.rb +14 -4
  11. data/lib/switchman/active_record/association.rb +64 -37
  12. data/lib/switchman/active_record/attribute_methods.rb +54 -22
  13. data/lib/switchman/active_record/base.rb +76 -31
  14. data/lib/switchman/active_record/batches.rb +3 -1
  15. data/lib/switchman/active_record/calculations.rb +17 -22
  16. data/lib/switchman/active_record/connection_handler.rb +88 -78
  17. data/lib/switchman/active_record/connection_pool.rb +28 -23
  18. data/lib/switchman/active_record/finder_methods.rb +37 -28
  19. data/lib/switchman/active_record/log_subscriber.rb +14 -19
  20. data/lib/switchman/active_record/migration.rb +80 -0
  21. data/lib/switchman/active_record/model_schema.rb +3 -1
  22. data/lib/switchman/active_record/persistence.rb +9 -1
  23. data/lib/switchman/active_record/postgresql_adapter.rb +170 -126
  24. data/lib/switchman/active_record/predicate_builder.rb +3 -1
  25. data/lib/switchman/active_record/query_cache.rb +22 -87
  26. data/lib/switchman/active_record/query_methods.rb +139 -125
  27. data/lib/switchman/active_record/reflection.rb +42 -14
  28. data/lib/switchman/active_record/relation.rb +108 -33
  29. data/lib/switchman/active_record/spawn_methods.rb +2 -0
  30. data/lib/switchman/active_record/statement_cache.rb +44 -52
  31. data/lib/switchman/active_record/table_definition.rb +4 -2
  32. data/lib/switchman/active_record/type_caster.rb +2 -0
  33. data/lib/switchman/active_record/where_clause_factory.rb +5 -2
  34. data/lib/switchman/active_support/cache.rb +18 -0
  35. data/lib/switchman/arel.rb +8 -25
  36. data/lib/switchman/call_super.rb +19 -0
  37. data/lib/switchman/connection_pool_proxy.rb +70 -24
  38. data/lib/switchman/database_server.rb +69 -59
  39. data/lib/switchman/default_shard.rb +3 -0
  40. data/lib/switchman/engine.rb +44 -41
  41. data/lib/switchman/environment.rb +2 -0
  42. data/lib/switchman/errors.rb +2 -0
  43. data/lib/switchman/{shackles → guard_rail}/relation.rb +7 -5
  44. data/lib/switchman/{shackles.rb → guard_rail.rb} +6 -4
  45. data/lib/switchman/open4.rb +2 -0
  46. data/lib/switchman/r_spec_helper.rb +14 -8
  47. data/lib/switchman/rails.rb +2 -0
  48. data/lib/switchman/schema_cache.rb +17 -0
  49. data/lib/switchman/sharded_instrumenter.rb +4 -2
  50. data/lib/switchman/standard_error.rb +4 -2
  51. data/lib/switchman/test_helper.rb +7 -10
  52. data/lib/switchman/version.rb +3 -1
  53. data/lib/switchman.rb +5 -1
  54. data/lib/tasks/switchman.rake +53 -72
  55. metadata +84 -38
  56. data/app/models/switchman/shard_internal.rb +0 -692
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Switchman
4
+ module ActiveRecord
5
+ module Migration
6
+ module Compatibility
7
+ module V5_0
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
19
+ end
20
+ end
21
+ end
22
+
23
+ def connection
24
+ 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
28
+ conn
29
+ end
30
+ end
31
+
32
+ module Migrator
33
+ # significant change: hash shard id, not database name
34
+ def generate_migrator_advisory_lock_id
35
+ shard_name_hash = Zlib.crc32(Shard.current.name)
36
+ ::ActiveRecord::Migrator::MIGRATOR_SALT * shard_name_hash
37
+ 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
+ end
70
+
71
+ module MigrationContext
72
+ def migrations
73
+ return @migrations if instance_variable_defined?(:@migrations)
74
+ migrations_cache = Thread.current[:migrations_cache] ||= {}
75
+ key = Digest::MD5.hexdigest(migration_files.sort.join(','))
76
+ @migrations = migrations_cache[key] ||= super
77
+ end
78
+ end
79
+ end
80
+ end
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Switchman
2
4
  module ActiveRecord
3
5
  module ModelSchema
4
6
  module ClassMethods
5
7
  def quoted_table_name
6
8
  @quoted_table_name ||= {}
7
- @quoted_table_name[Shard.current.id] ||= connection.quote_table_name(table_name)
9
+ @quoted_table_name[Shard.current(shard_category).id] ||= connection.quote_table_name(table_name)
8
10
  end
9
11
  end
10
12
  end
@@ -1,10 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Switchman
2
4
  module ActiveRecord
3
5
  module Persistence
4
6
  # touch reads the id attribute directly, so it's not relative to the current shard
5
- def touch(*)
7
+ def touch(*, **)
6
8
  shard.activate(self.class.shard_category) { super }
7
9
  end
10
+
11
+ if ::Rails.version >= '5.2'
12
+ def update_columns(*)
13
+ shard.activate(self.class.shard_category) { super }
14
+ end
15
+ end
8
16
  end
9
17
  end
10
18
  end
@@ -1,10 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Switchman
2
4
  module ActiveRecord
3
5
  module PostgreSQLAdapter
4
- def self.prepended(klass)
5
- klass::NATIVE_DATABASE_TYPES[:primary_key] = "bigserial primary key".freeze
6
- end
7
-
8
6
  # copy/paste; use quote_local_table_name
9
7
  def create_database(name, options = {})
10
8
  options = { encoding: 'utf8' }.merge!(options.symbolize_keys)
@@ -42,149 +40,119 @@ module Switchman
42
40
  select_values("SELECT * FROM unnest(current_schemas(false))")
43
41
  end
44
42
 
45
- def use_qualified_names?
46
- @config[:use_qualified_names]
43
+ def extract_schema_qualified_name(string)
44
+ 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
48
+ [name.schema, name.identifier]
47
49
  end
48
50
 
49
- def tables(name = nil)
50
- schema = shard.name if use_qualified_names?
51
-
52
- query(<<-SQL, 'SCHEMA').map { |row| row[0] }
53
- SELECT tablename
54
- FROM pg_tables
55
- WHERE schemaname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'}
56
- SQL
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
68
  end
58
69
 
59
- def table_exists?(name)
60
- if ::Rails.version < '4.2'
61
- schema, table = ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::Utils.extract_schema_and_table(name.to_s)
62
- return false unless table
63
- schema ||= shard.name if use_qualified_names?
64
-
65
- binds = [[nil, table]]
66
- binds << [nil, schema] if schema
67
-
68
- exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
69
- SELECT COUNT(*)
70
- FROM pg_class c
71
- LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
72
- WHERE c.relkind in ('v','r')
73
- AND c.relname = '#{table.gsub(/(^"|"$)/,'')}'
74
- AND n.nspname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'}
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}'
75
76
  SQL
76
- else
77
- name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(name.to_s)
77
+ end
78
+
79
+ def view_exists?(name)
80
+ name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(name.to_s)
78
81
  return false unless name.identifier
79
- if !name.schema && use_qualified_names?
82
+ if !name.schema
80
83
  name.instance_variable_set(:@schema, shard.name)
81
84
  end
82
85
 
83
- exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
84
- SELECT COUNT(*)
85
- FROM pg_class c
86
- LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
87
- WHERE c.relkind IN ('r','v','m') -- (r)elation/table, (v)iew, (m)aterialized view
88
- AND c.relname = '#{name.identifier}'
89
- AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'}
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}'
90
93
  SQL
91
94
  end
92
- end
93
-
94
- def indexes(table_name)
95
- schema = shard.name if use_qualified_names?
96
-
97
- result = query(<<-SQL, 'SCHEMA')
98
- SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid
99
- FROM pg_class t
100
- INNER JOIN pg_index d ON t.oid = d.indrelid
101
- INNER JOIN pg_class i ON d.indexrelid = i.oid
102
- WHERE i.relkind = 'i'
103
- AND d.indisprimary = 'f'
104
- AND t.relname = '#{table_name}'
105
- AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'} )
106
- ORDER BY i.relname
107
- SQL
108
-
109
-
110
- result.map do |row|
111
- index_name = row[0]
112
- unique = 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
95
 
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
- ::ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using)
134
- end
135
- end.compact
136
- end
137
-
138
- def index_name_exists?(table_name, index_name, default)
139
- schema = shard.name if use_qualified_names?
140
-
141
- exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
142
- SELECT COUNT(*)
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
143
99
  FROM pg_class t
144
100
  INNER JOIN pg_index d ON t.oid = d.indrelid
145
101
  INNER JOIN pg_class i ON d.indexrelid = i.oid
146
102
  WHERE i.relkind = 'i'
147
- AND i.relname = '#{index_name}'
103
+ AND d.indisprimary = 'f'
148
104
  AND t.relname = '#{table_name}'
149
- AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'} )
150
- SQL
151
- end
152
-
153
- def quote_local_table_name(name)
154
- # postgres quotes tables and columns the same; just pass through
155
- # (differs from quote_table_name below by no logic to explicitly
156
- # qualify the table)
157
- quote_column_name(name)
158
- end
159
-
160
- def quote_table_name name
161
- if ::Rails.version < '4.2'.freeze
162
- schema, name_part = extract_pg_identifier_from_name(name.to_s)
105
+ AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = '#{shard.name}' )
106
+ ORDER BY i.relname
107
+ SQL
163
108
 
164
- if !name_part && use_qualified_names? && shard.name
165
- schema, name_part = shard.name, schema
166
- end
167
109
 
168
- unless name_part
169
- quote_column_name(schema)
170
- else
171
- table_name, name_part = extract_pg_identifier_from_name(name_part)
172
- "#{quote_column_name(schema)}.#{quote_column_name(table_name)}"
173
- end
174
- else
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
175
141
 
176
- name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(name.to_s)
177
- if !name.schema && use_qualified_names?
178
- name.instance_variable_set(:@schema, shard.name)
179
- end
180
- name.quoted
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
181
153
  end
182
- end
183
154
 
184
- if ::Rails.version >= '4.2'
185
155
  def foreign_keys(table_name)
186
- schema = shard.name if use_qualified_names?
187
-
188
156
  # mostly copy-pasted from AR - only change is to the nspname condition for qualified names support
189
157
  fk_info = select_all <<-SQL.strip_heredoc
190
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
@@ -196,7 +164,7 @@ module Switchman
196
164
  JOIN pg_namespace t3 ON c.connamespace = t3.oid
197
165
  WHERE c.contype = 'f'
198
166
  AND t1.relname = #{quote(table_name)}
199
- AND t3.nspname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'}
167
+ AND t3.nspname = '#{shard.name}'
200
168
  ORDER BY c.conname
201
169
  SQL
202
170
 
@@ -210,9 +178,85 @@ module Switchman
210
178
  options[:on_delete] = extract_foreign_key_action(row['on_delete'])
211
179
  options[:on_update] = extract_foreign_key_action(row['on_update'])
212
180
 
213
- ::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(table_name, row['to_table'], options)
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)
214
189
  end
215
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
200
+ end
201
+
202
+ def quote_local_table_name(name)
203
+ # postgres quotes tables and columns the same; just pass through
204
+ # (differs from quote_table_name_with_shard below by no logic to
205
+ # explicitly qualify the table)
206
+ quote_column_name(name)
207
+ end
208
+
209
+ def quote_table_name(name)
210
+ return quote_local_table_name(name) if @use_local_table_name
211
+ 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
215
+ name.quoted
216
+ end
217
+
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)
223
+ old_value = @use_local_table_name
224
+ @use_local_table_name = enable
225
+ yield
226
+ ensure
227
+ @use_local_table_name = old_value
228
+ end
229
+
230
+ 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]
234
+ end
235
+
236
+ def rename_table(table_name, new_name)
237
+ clear_cache!
238
+ execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_local_table_name(new_name)}"
239
+ pk, seq = pk_and_sequence_for(new_name)
240
+ if pk
241
+ idx = "#{table_name}_pkey"
242
+ new_idx = "#{new_name}_pkey"
243
+ execute "ALTER INDEX #{quote_table_name(idx)} RENAME TO #{quote_local_table_name(new_idx)}"
244
+ if seq && seq.identifier == "#{table_name}_#{pk}_seq"
245
+ new_seq = "#{new_name}_#{pk}_seq"
246
+ execute "ALTER TABLE #{seq.quoted} RENAME TO #{quote_local_table_name(new_seq)}"
247
+ end
248
+ end
249
+ rename_table_indexes(table_name, new_name)
250
+ end
251
+
252
+ def rename_index(table_name, old_name, new_name)
253
+ validate_index_length!(table_name, new_name)
254
+
255
+ execute "ALTER INDEX #{quote_table_name(old_name)} RENAME TO #{quote_local_table_name(new_name)}"
256
+ end
257
+
258
+ def columns(*)
259
+ with_local_table_name(false) { super }
216
260
  end
217
261
  end
218
262
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Switchman
2
4
  module ActiveRecord
3
5
  module PredicateBuilder
@@ -13,7 +15,7 @@ module Switchman
13
15
  def convert_to_id(value)
14
16
  case value
15
17
  when ::ActiveRecord::Base
16
- value.send(primary_key)
18
+ value.id
17
19
  else
18
20
  super
19
21
  end
@@ -1,99 +1,34 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Switchman
2
4
  module ActiveRecord
3
5
  module QueryCache
4
- # thread local accessors to replace @query_cache_enabled
5
- def query_cache
6
- thread_cache = Thread.current[:query_cache] ||= {}
7
- thread_cache[self.object_id] ||= Hash.new { |h,sql| h[sql] = {} }
8
- end
9
-
10
- def query_cache_enabled
11
- Thread.current[:query_cache_enabled]
12
- end
13
-
14
- def query_cache_enabled=(value)
15
- Thread.current[:query_cache_enabled] = value
16
- end
17
-
18
- # basically wholesale repeat of the methods from the original (see
19
- # https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb),
20
- # but with self.query_cache_enabled and self.query_cache_enabled= instead
21
- # of @query_cache_enabled.
22
-
23
- def enable_query_cache!
24
- self.query_cache_enabled = true
25
- end
26
-
27
- def disable_query_cache!
28
- self.query_cache_enabled = false
29
- end
30
-
31
- def cache
32
- old, self.query_cache_enabled = query_cache_enabled, true
33
- yield
34
- ensure
35
- self.query_cache_enabled = old
36
- clear_query_cache unless self.query_cache_enabled
37
- end
38
-
39
- def uncached
40
- old, self.query_cache_enabled = query_cache_enabled, false
41
- yield
42
- ensure
43
- self.query_cache_enabled = old
44
- end
45
-
46
- def clear_query_cache
47
- Thread.current[:query_cache].try(:clear)
48
- end
49
-
50
- def select_all(arel, name = nil, binds = [], preparable: nil)
51
- if self.query_cache_enabled && !locked?(arel)
52
- arel, binds = binds_from_relation(arel, binds)
53
- sql = to_sql(arel, binds)
54
- if ::Rails.version >= '5'
55
- cache_sql(sql, binds) { super(sql, name, binds, preparable: preparable) }
56
- else
57
- cache_sql(sql, binds) { super(sql, name, binds) }
58
- end
59
- else
60
- if ::Rails.version >= '5'
61
- super
62
- else
63
- super(arel, name, binds)
64
- end
65
- end
66
- end
67
-
68
- # no reason to define these on the including class directly. the super
69
- # works just as well from a method on the included module
70
- [:insert, :update, :delete].each do |method_name|
71
- class_eval <<-end_code, __FILE__, __LINE__ + 1
72
- def #{method_name}(*args)
73
- clear_query_cache if self.query_cache_enabled
74
- super
75
- end
76
- end_code
77
- end
78
6
 
79
7
  private
80
8
 
81
- def cache_sql(sql, binds)
9
+ def cache_sql(sql, name, binds)
82
10
  # have to include the shard id in the cache key because of switching dbs on the same connection
83
11
  sql = "#{self.shard.id}::#{sql}"
84
- result =
85
- if query_cache[sql].key?(binds)
86
- ::ActiveSupport::Notifications.instrument("sql.active_record",
87
- :sql => sql, :binds => binds, :name => "CACHE", :connection_id => object_id)
88
- query_cache[sql][binds]
89
- else
90
- query_cache[sql][binds] = yield
91
- end
92
-
93
- if ::ActiveRecord::Result === result
12
+ @lock.synchronize do
13
+ result =
14
+ if query_cache[sql].key?(binds)
15
+ args = {
16
+ sql: sql,
17
+ binds: binds,
18
+ name: name,
19
+ connection_id: object_id,
20
+ cached: true
21
+ }
22
+ args[:type_casted_binds] = -> { type_casted_binds(binds) } if ::Rails.version >= '5.1.5'
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
94
31
  result.dup
95
- else
96
- result.collect { |row| row.dup }
97
32
  end
98
33
  end
99
34
  end