switchman 1.6.1 → 2.0.9

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 +746 -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 +16 -5
  13. data/lib/switchman/active_record/base.rb +67 -22
  14. data/lib/switchman/active_record/batches.rb +3 -1
  15. data/lib/switchman/active_record/calculations.rb +16 -21
  16. data/lib/switchman/active_record/connection_handler.rb +71 -79
  17. data/lib/switchman/active_record/connection_pool.rb +28 -23
  18. data/lib/switchman/active_record/finder_methods.rb +20 -29
  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 +166 -126
  24. data/lib/switchman/active_record/predicate_builder.rb +2 -0
  25. data/lib/switchman/active_record/query_cache.rb +22 -87
  26. data/lib/switchman/active_record/query_methods.rb +122 -126
  27. data/lib/switchman/active_record/reflection.rb +37 -20
  28. data/lib/switchman/active_record/relation.rb +50 -29
  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 +42 -40
  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 +6 -3
  52. data/lib/switchman/version.rb +3 -1
  53. data/lib/switchman.rb +3 -1
  54. data/lib/tasks/switchman.rake +46 -72
  55. metadata +87 -41
  56. data/app/models/switchman/shard_internal.rb +0 -692
@@ -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
-
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
95
 
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,81 @@ 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)
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
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_local_table_name(enable = true)
219
+ old_value = @use_local_table_name
220
+ @use_local_table_name = enable
221
+ yield
222
+ ensure
223
+ @use_local_table_name = old_value
224
+ end
225
+
226
+ def add_index_options(_table_name, _column_name, **)
227
+ index_name, index_type, index_columns, index_options, algorithm, using = super
228
+ algorithm = nil if DatabaseServer.creating_new_shard && algorithm == "CONCURRENTLY"
229
+ [index_name, index_type, index_columns, index_options, algorithm, using]
230
+ end
231
+
232
+ def rename_table(table_name, new_name)
233
+ clear_cache!
234
+ execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_local_table_name(new_name)}"
235
+ pk, seq = pk_and_sequence_for(new_name)
236
+ if pk
237
+ idx = "#{table_name}_pkey"
238
+ new_idx = "#{new_name}_pkey"
239
+ execute "ALTER INDEX #{quote_table_name(idx)} RENAME TO #{quote_local_table_name(new_idx)}"
240
+ if seq && seq.identifier == "#{table_name}_#{pk}_seq"
241
+ new_seq = "#{new_name}_#{pk}_seq"
242
+ execute "ALTER TABLE #{seq.quoted} RENAME TO #{quote_local_table_name(new_seq)}"
214
243
  end
215
244
  end
245
+ rename_table_indexes(table_name, new_name)
246
+ end
247
+
248
+ def rename_index(table_name, old_name, new_name)
249
+ validate_index_length!(table_name, new_name)
250
+
251
+ execute "ALTER INDEX #{quote_table_name(old_name)} RENAME TO #{quote_local_table_name(new_name)}"
252
+ end
253
+
254
+ def columns(*)
255
+ with_local_table_name(false) { super }
216
256
  end
217
257
  end
218
258
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Switchman
2
4
  module ActiveRecord
3
5
  module PredicateBuilder
@@ -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