activerecord 8.0.2 → 8.1.0.beta1

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 (159) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +459 -413
  3. data/README.rdoc +2 -2
  4. data/lib/active_record/association_relation.rb +1 -1
  5. data/lib/active_record/associations/association.rb +1 -1
  6. data/lib/active_record/associations/belongs_to_association.rb +9 -1
  7. data/lib/active_record/associations/builder/association.rb +16 -5
  8. data/lib/active_record/associations/builder/belongs_to.rb +17 -4
  9. data/lib/active_record/associations/builder/collection_association.rb +7 -3
  10. data/lib/active_record/associations/builder/has_one.rb +1 -1
  11. data/lib/active_record/associations/builder/singular_association.rb +33 -5
  12. data/lib/active_record/associations/collection_association.rb +3 -3
  13. data/lib/active_record/associations/collection_proxy.rb +22 -4
  14. data/lib/active_record/associations/deprecation.rb +88 -0
  15. data/lib/active_record/associations/errors.rb +3 -0
  16. data/lib/active_record/associations/join_dependency.rb +2 -0
  17. data/lib/active_record/associations/preloader/branch.rb +1 -0
  18. data/lib/active_record/associations.rb +159 -21
  19. data/lib/active_record/attribute_methods/query.rb +34 -0
  20. data/lib/active_record/attribute_methods/serialization.rb +17 -4
  21. data/lib/active_record/attributes.rb +38 -24
  22. data/lib/active_record/base.rb +0 -1
  23. data/lib/active_record/coders/json.rb +14 -5
  24. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +2 -4
  25. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +15 -0
  26. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +51 -12
  27. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +384 -49
  28. data/lib/active_record/connection_adapters/abstract/database_statements.rb +26 -30
  29. data/lib/active_record/connection_adapters/abstract/query_cache.rb +19 -1
  30. data/lib/active_record/connection_adapters/abstract/quoting.rb +15 -24
  31. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +7 -2
  32. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +26 -34
  33. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +2 -1
  34. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +89 -23
  35. data/lib/active_record/connection_adapters/abstract/transaction.rb +16 -3
  36. data/lib/active_record/connection_adapters/abstract_adapter.rb +67 -13
  37. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +43 -11
  38. data/lib/active_record/connection_adapters/column.rb +17 -4
  39. data/lib/active_record/connection_adapters/mysql/database_statements.rb +4 -4
  40. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +2 -0
  41. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +42 -5
  42. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +26 -4
  43. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +27 -22
  44. data/lib/active_record/connection_adapters/mysql2_adapter.rb +1 -0
  45. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +18 -16
  46. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +2 -2
  47. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +1 -1
  48. data/lib/active_record/connection_adapters/postgresql/quoting.rb +21 -10
  49. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +1 -1
  50. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +8 -21
  51. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +65 -30
  52. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +74 -38
  53. data/lib/active_record/connection_adapters/postgresql_adapter.rb +12 -7
  54. data/lib/active_record/connection_adapters/schema_cache.rb +2 -2
  55. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +39 -27
  56. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +0 -8
  57. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +4 -13
  58. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +56 -32
  59. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +4 -3
  60. data/lib/active_record/connection_adapters/trilogy_adapter.rb +1 -1
  61. data/lib/active_record/connection_adapters.rb +1 -0
  62. data/lib/active_record/connection_handling.rb +1 -1
  63. data/lib/active_record/core.rb +13 -10
  64. data/lib/active_record/counter_cache.rb +33 -8
  65. data/lib/active_record/database_configurations/database_config.rb +5 -1
  66. data/lib/active_record/database_configurations/hash_config.rb +56 -9
  67. data/lib/active_record/database_configurations/url_config.rb +13 -3
  68. data/lib/active_record/database_configurations.rb +7 -3
  69. data/lib/active_record/delegated_type.rb +2 -2
  70. data/lib/active_record/dynamic_matchers.rb +54 -69
  71. data/lib/active_record/encryption/encryptable_record.rb +5 -5
  72. data/lib/active_record/encryption/encrypted_attribute_type.rb +2 -2
  73. data/lib/active_record/encryption/encryptor.rb +27 -25
  74. data/lib/active_record/encryption/scheme.rb +1 -1
  75. data/lib/active_record/enum.rb +37 -20
  76. data/lib/active_record/errors.rb +20 -4
  77. data/lib/active_record/explain_registry.rb +0 -1
  78. data/lib/active_record/filter_attribute_handler.rb +73 -0
  79. data/lib/active_record/fixture_set/table_row.rb +19 -2
  80. data/lib/active_record/fixtures.rb +2 -2
  81. data/lib/active_record/gem_version.rb +3 -3
  82. data/lib/active_record/inheritance.rb +1 -1
  83. data/lib/active_record/insert_all.rb +12 -7
  84. data/lib/active_record/locking/optimistic.rb +7 -0
  85. data/lib/active_record/locking/pessimistic.rb +5 -0
  86. data/lib/active_record/log_subscriber.rb +1 -5
  87. data/lib/active_record/middleware/shard_selector.rb +34 -17
  88. data/lib/active_record/migration/command_recorder.rb +14 -1
  89. data/lib/active_record/migration/compatibility.rb +34 -24
  90. data/lib/active_record/migration/default_schema_versions_formatter.rb +30 -0
  91. data/lib/active_record/migration.rb +31 -21
  92. data/lib/active_record/model_schema.rb +10 -7
  93. data/lib/active_record/nested_attributes.rb +2 -0
  94. data/lib/active_record/persistence.rb +34 -3
  95. data/lib/active_record/query_cache.rb +22 -15
  96. data/lib/active_record/query_logs.rb +7 -7
  97. data/lib/active_record/querying.rb +4 -4
  98. data/lib/active_record/railtie.rb +34 -5
  99. data/lib/active_record/railties/databases.rake +23 -19
  100. data/lib/active_record/railties/job_checkpoints.rb +15 -0
  101. data/lib/active_record/railties/job_runtime.rb +10 -11
  102. data/lib/active_record/reflection.rb +42 -3
  103. data/lib/active_record/relation/batches.rb +26 -12
  104. data/lib/active_record/relation/calculations.rb +35 -25
  105. data/lib/active_record/relation/delegation.rb +0 -1
  106. data/lib/active_record/relation/finder_methods.rb +41 -24
  107. data/lib/active_record/relation/merger.rb +2 -2
  108. data/lib/active_record/relation/predicate_builder.rb +2 -2
  109. data/lib/active_record/relation/query_attribute.rb +3 -1
  110. data/lib/active_record/relation/query_methods.rb +43 -33
  111. data/lib/active_record/relation/spawn_methods.rb +6 -6
  112. data/lib/active_record/relation/where_clause.rb +7 -10
  113. data/lib/active_record/relation.rb +37 -15
  114. data/lib/active_record/result.rb +44 -21
  115. data/lib/active_record/sanitization.rb +2 -0
  116. data/lib/active_record/schema_dumper.rb +12 -10
  117. data/lib/active_record/scoping.rb +0 -1
  118. data/lib/active_record/secure_token.rb +3 -3
  119. data/lib/active_record/signed_id.rb +46 -18
  120. data/lib/active_record/statement_cache.rb +13 -9
  121. data/lib/active_record/store.rb +44 -19
  122. data/lib/active_record/tasks/abstract_tasks.rb +76 -0
  123. data/lib/active_record/tasks/database_tasks.rb +24 -35
  124. data/lib/active_record/tasks/mysql_database_tasks.rb +3 -40
  125. data/lib/active_record/tasks/postgresql_database_tasks.rb +14 -40
  126. data/lib/active_record/tasks/sqlite_database_tasks.rb +14 -26
  127. data/lib/active_record/test_databases.rb +11 -3
  128. data/lib/active_record/test_fixtures.rb +27 -2
  129. data/lib/active_record/testing/query_assertions.rb +8 -2
  130. data/lib/active_record/timestamp.rb +4 -2
  131. data/lib/active_record/transaction.rb +2 -5
  132. data/lib/active_record/transactions.rb +34 -10
  133. data/lib/active_record/type/hash_lookup_type_map.rb +2 -1
  134. data/lib/active_record/type/internal/timezone.rb +7 -0
  135. data/lib/active_record/type/json.rb +15 -2
  136. data/lib/active_record/type/serialized.rb +11 -4
  137. data/lib/active_record/type/type_map.rb +1 -1
  138. data/lib/active_record/type_caster/connection.rb +2 -1
  139. data/lib/active_record/validations/associated.rb +1 -1
  140. data/lib/active_record.rb +68 -5
  141. data/lib/arel/alias_predication.rb +2 -0
  142. data/lib/arel/crud.rb +8 -11
  143. data/lib/arel/delete_manager.rb +5 -0
  144. data/lib/arel/nodes/count.rb +2 -2
  145. data/lib/arel/nodes/delete_statement.rb +4 -2
  146. data/lib/arel/nodes/function.rb +4 -10
  147. data/lib/arel/nodes/named_function.rb +2 -2
  148. data/lib/arel/nodes/node.rb +1 -1
  149. data/lib/arel/nodes/update_statement.rb +4 -2
  150. data/lib/arel/nodes.rb +0 -2
  151. data/lib/arel/select_manager.rb +13 -4
  152. data/lib/arel/update_manager.rb +5 -0
  153. data/lib/arel/visitors/dot.rb +2 -3
  154. data/lib/arel/visitors/postgresql.rb +55 -0
  155. data/lib/arel/visitors/sqlite.rb +55 -8
  156. data/lib/arel/visitors/to_sql.rb +5 -21
  157. data/lib/arel.rb +3 -1
  158. metadata +15 -11
  159. data/lib/active_record/normalization.rb +0 -163
@@ -29,6 +29,11 @@ module ActiveRecord
29
29
  # ...
30
30
  # ]
31
31
  #
32
+ # # Get the number of rows affected by the query:
33
+ # result = ActiveRecord::Base.lease_connection.exec_query('INSERT INTO posts (title, body) VALUES ("title_3", "body_3"), ("title_4", "body_4")')
34
+ # result.affected_rows
35
+ # # => 2
36
+ #
32
37
  # # ActiveRecord::Result also includes Enumerable.
33
38
  # result.each do |row|
34
39
  # puts row['title'] + " " + row['body']
@@ -89,24 +94,26 @@ module ActiveRecord
89
94
  alias_method :to_hash, :to_h
90
95
  end
91
96
 
92
- attr_reader :columns, :rows, :column_types
97
+ attr_reader :columns, :rows, :affected_rows
93
98
 
94
- def self.empty(async: false) # :nodoc:
99
+ def self.empty(async: false, affected_rows: nil) # :nodoc:
95
100
  if async
96
- EMPTY_ASYNC
101
+ FutureResult.wrap(new(EMPTY_ARRAY, EMPTY_ARRAY, EMPTY_HASH, affected_rows: affected_rows)).freeze
97
102
  else
98
- EMPTY
103
+ new(EMPTY_ARRAY, EMPTY_ARRAY, EMPTY_HASH, affected_rows: affected_rows).freeze
99
104
  end
100
105
  end
101
106
 
102
- def initialize(columns, rows, column_types = nil)
107
+ def initialize(columns, rows, column_types = nil, affected_rows: nil)
103
108
  # We freeze the strings to prevent them getting duped when
104
109
  # used as keys in ActiveRecord::Base's @attributes hash
105
110
  @columns = columns.each(&:-@).freeze
106
111
  @rows = rows
107
112
  @hash_rows = nil
108
- @column_types = column_types || EMPTY_HASH
113
+ @column_types = column_types.freeze
114
+ @types_hash = nil
109
115
  @column_indexes = nil
116
+ @affected_rows = affected_rows
110
117
  end
111
118
 
112
119
  # Returns true if this result set includes the column named +name+
@@ -154,6 +161,24 @@ module ActiveRecord
154
161
  n ? hash_rows.last(n) : hash_rows.last
155
162
  end
156
163
 
164
+ # Returns the +ActiveRecord::Type+ type of all columns.
165
+ # Note that not all database adapters return the result types,
166
+ # so the hash may be empty.
167
+ def column_types
168
+ if @column_types
169
+ @types_hash ||= begin
170
+ types = {}
171
+ @columns.each_with_index do |name, index|
172
+ type = @column_types[index] || Type.default_value
173
+ types[name] = types[index] = type
174
+ end
175
+ types.freeze
176
+ end
177
+ else
178
+ EMPTY_HASH
179
+ end
180
+ end
181
+
157
182
  def result # :nodoc:
158
183
  self
159
184
  end
@@ -162,7 +187,7 @@ module ActiveRecord
162
187
  self
163
188
  end
164
189
 
165
- def cast_values(type_overrides = {}) # :nodoc:
190
+ def cast_values(type_overrides = nil) # :nodoc:
166
191
  if columns.one?
167
192
  # Separated to avoid allocating an array per row
168
193
 
@@ -190,13 +215,13 @@ module ActiveRecord
190
215
 
191
216
  def initialize_copy(other)
192
217
  @rows = rows.dup
193
- @column_types = column_types.dup
194
218
  @hash_rows = nil
195
219
  end
196
220
 
197
221
  def freeze # :nodoc:
198
222
  hash_rows.freeze
199
- indexed_rows.freeze
223
+ indexed_rows
224
+ column_types
200
225
  super
201
226
  end
202
227
 
@@ -204,7 +229,7 @@ module ActiveRecord
204
229
  @column_indexes ||= begin
205
230
  index = 0
206
231
  hash = {}
207
- length = columns.length
232
+ length = columns.length
208
233
  while index < length
209
234
  hash[columns[index]] = index
210
235
  index += 1
@@ -222,10 +247,14 @@ module ActiveRecord
222
247
 
223
248
  private
224
249
  def column_type(name, index, type_overrides)
225
- type_overrides.fetch(name) do
226
- column_types.fetch(index) do
227
- column_types.fetch(name, Type.default_value)
250
+ if type_overrides
251
+ type_overrides.fetch(name) do
252
+ column_type(name, index, nil)
228
253
  end
254
+ elsif @column_types
255
+ @column_types[index] || Type.default_value
256
+ else
257
+ Type.default_value
229
258
  end
230
259
  end
231
260
 
@@ -237,14 +266,8 @@ module ActiveRecord
237
266
  end
238
267
  end
239
268
 
240
- empty_array = [].freeze
269
+ EMPTY_ARRAY = [].freeze
241
270
  EMPTY_HASH = {}.freeze
242
- private_constant :EMPTY_HASH
243
-
244
- EMPTY = new(empty_array, empty_array, EMPTY_HASH).freeze
245
- private_constant :EMPTY
246
-
247
- EMPTY_ASYNC = FutureResult.wrap(EMPTY).freeze
248
- private_constant :EMPTY_ASYNC
271
+ private_constant :EMPTY_ARRAY, :EMPTY_HASH
249
272
  end
250
273
  end
@@ -161,6 +161,8 @@ module ActiveRecord
161
161
  #
162
162
  # sanitize_sql_array(["role = ?", 0])
163
163
  # # => "role = '0'"
164
+ #
165
+ # Before using this method, please consider if Arel.sql would be better for your use-case
164
166
  def sanitize_sql_array(ary)
165
167
  statement, *values = ary
166
168
  if values.first.is_a?(Hash) && /:\w+/.match?(statement)
@@ -165,7 +165,7 @@ module ActiveRecord
165
165
  # first dump primary key column
166
166
  pk = @connection.primary_key(table)
167
167
 
168
- tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}"
168
+ tbl.print " create_table #{relation_name(remove_prefix_and_suffix(table)).inspect}"
169
169
 
170
170
  case pk
171
171
  when String
@@ -192,7 +192,7 @@ module ActiveRecord
192
192
  tbl.puts ", force: :cascade do |t|"
193
193
 
194
194
  # then dump all non-primary key columns
195
- columns.each do |column|
195
+ columns.sort_by(&:name).each do |column|
196
196
  raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type)
197
197
  next if column.name == pk
198
198
 
@@ -233,7 +233,7 @@ module ActiveRecord
233
233
  if (indexes = @connection.indexes(table)).any?
234
234
  add_index_statements = indexes.map do |index|
235
235
  table_name = remove_prefix_and_suffix(index.table).inspect
236
- " add_index #{([table_name] + index_parts(index)).join(', ')}"
236
+ " add_index #{([relation_name(table_name)] + index_parts(index)).join(', ')}"
237
237
  end
238
238
 
239
239
  stream.puts add_index_statements.sort.join("\n")
@@ -277,6 +277,7 @@ module ActiveRecord
277
277
  index_parts << "nulls_not_distinct: #{index.nulls_not_distinct.inspect}" if index.nulls_not_distinct
278
278
  index_parts << "type: #{index.type.inspect}" if index.type
279
279
  index_parts << "comment: #{index.comment.inspect}" if index.comment
280
+ index_parts << "enabled: #{index.enabled.inspect}" if @connection.supports_disabling_indexes? && index.disabled?
280
281
  index_parts
281
282
  end
282
283
 
@@ -317,8 +318,8 @@ module ActiveRecord
317
318
  if (foreign_keys = @connection.foreign_keys(table)).any?
318
319
  add_foreign_key_statements = foreign_keys.map do |foreign_key|
319
320
  parts = [
320
- "add_foreign_key #{remove_prefix_and_suffix(foreign_key.from_table).inspect}",
321
- remove_prefix_and_suffix(foreign_key.to_table).inspect,
321
+ relation_name(remove_prefix_and_suffix(foreign_key.from_table)).inspect,
322
+ relation_name(remove_prefix_and_suffix(foreign_key.to_table)).inspect,
322
323
  ]
323
324
 
324
325
  if foreign_key.column != @connection.foreign_key_column_for(foreign_key.to_table, "id")
@@ -329,16 +330,13 @@ module ActiveRecord
329
330
  parts << "primary_key: #{foreign_key.primary_key.inspect}"
330
331
  end
331
332
 
332
- if foreign_key.export_name_on_schema_dump?
333
- parts << "name: #{foreign_key.name.inspect}"
334
- end
335
-
333
+ parts << "name: #{foreign_key.name.inspect}" if foreign_key.export_name_on_schema_dump?
336
334
  parts << "on_update: #{foreign_key.on_update.inspect}" if foreign_key.on_update
337
335
  parts << "on_delete: #{foreign_key.on_delete.inspect}" if foreign_key.on_delete
338
336
  parts << "deferrable: #{foreign_key.deferrable.inspect}" if foreign_key.deferrable
339
337
  parts << "validate: #{foreign_key.validate?.inspect}" unless foreign_key.validate?
340
338
 
341
- " #{parts.join(', ')}"
339
+ " add_foreign_key #{parts.join(', ')}"
342
340
  end
343
341
 
344
342
  stream.puts add_foreign_key_statements.sort.join("\n")
@@ -363,6 +361,10 @@ module ActiveRecord
363
361
  end
364
362
  end
365
363
 
364
+ def relation_name(name)
365
+ name
366
+ end
367
+
366
368
  def remove_prefix_and_suffix(table)
367
369
  # This method appears at the top when profiling active_record test cases run.
368
370
  # Avoid costly calculation when there are no prefix and suffix.
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/module/delegation"
4
3
 
5
4
  module ActiveRecord
6
5
  module Scoping
@@ -30,13 +30,13 @@ module ActiveRecord
30
30
  # {validates_uniqueness_of}[rdoc-ref:Validations::ClassMethods#validates_uniqueness_of] can.
31
31
  # You're encouraged to add a unique index in the database to deal with this even more unlikely scenario.
32
32
  #
33
- # === Options
33
+ # ==== Options
34
34
  #
35
- # [:length]
35
+ # [+:length+]
36
36
  # Length of the Secure Random, with a minimum of 24 characters. It will
37
37
  # default to 24.
38
38
  #
39
- # [:on]
39
+ # [+:on+]
40
40
  # The callback when the value is generated. When called with <tt>on:
41
41
  # :initialize</tt>, the value is generated in an
42
42
  # <tt>after_initialize</tt> callback, otherwise the value will be used
@@ -6,11 +6,27 @@ module ActiveRecord
6
6
  extend ActiveSupport::Concern
7
7
 
8
8
  included do
9
+ class_attribute :_signed_id_verifier, instance_accessor: false, instance_predicate: false
10
+
9
11
  ##
10
12
  # :singleton-method:
11
13
  # Set the secret used for the signed id verifier instance when using Active Record outside of \Rails.
12
14
  # Within \Rails, this is automatically set using the \Rails application key generator.
13
15
  class_attribute :signed_id_verifier_secret, instance_writer: false
16
+ module DeprecateSignedIdVerifierSecret
17
+ def signed_id_verifier_secret=(secret)
18
+ ActiveRecord.deprecator.warn(<<~MSG)
19
+ ActiveRecord::Base.signed_id_verifier_secret is deprecated and will be removed in Rails 8.2.
20
+
21
+ If the secret is model-specific, set Model.signed_id_verifier instead.
22
+
23
+ Otherwise, configure Rails.application.message_verifiers (or ActiveRecord.message_verifiers) with the secret.
24
+ MSG
25
+
26
+ super
27
+ end
28
+ end
29
+ singleton_class.prepend DeprecateSignedIdVerifierSecret
14
30
  end
15
31
 
16
32
  module RelationMethods # :nodoc:
@@ -49,46 +65,54 @@ module ActiveRecord
49
65
  #
50
66
  # travel_back
51
67
  # User.find_signed signed_id, purpose: :password_reset # => User.first
52
- def find_signed(signed_id, purpose: nil)
68
+ def find_signed(signed_id, purpose: nil, on_rotation: nil)
53
69
  raise UnknownPrimaryKey.new(self) if primary_key.nil?
54
70
 
55
- if id = signed_id_verifier.verified(signed_id, purpose: combine_signed_id_purposes(purpose))
71
+ options = { on_rotation: on_rotation }.compact
72
+ if id = signed_id_verifier.verified(signed_id, purpose: combine_signed_id_purposes(purpose), **options)
56
73
  find_by primary_key => id
57
74
  end
58
75
  end
59
76
 
60
- # Works like find_signed, but will raise an +ActiveSupport::MessageVerifier::InvalidSignature+
77
+ # Works like find_signed, but will raise an ActiveSupport::MessageVerifier::InvalidSignature
61
78
  # exception if the +signed_id+ has either expired, has a purpose mismatch, is for another record,
62
- # or has been tampered with. It will also raise an +ActiveRecord::RecordNotFound+ exception if
79
+ # or has been tampered with. It will also raise an ActiveRecord::RecordNotFound exception if
63
80
  # the valid signed id can't find a record.
64
81
  #
65
- # === Examples
82
+ # ==== Examples
66
83
  #
67
84
  # User.find_signed! "bad data" # => ActiveSupport::MessageVerifier::InvalidSignature
68
85
  #
69
86
  # signed_id = User.first.signed_id
70
87
  # User.first.destroy
71
88
  # User.find_signed! signed_id # => ActiveRecord::RecordNotFound
72
- def find_signed!(signed_id, purpose: nil)
73
- if id = signed_id_verifier.verify(signed_id, purpose: combine_signed_id_purposes(purpose))
89
+ def find_signed!(signed_id, purpose: nil, on_rotation: nil)
90
+ options = { on_rotation: on_rotation }.compact
91
+ if id = signed_id_verifier.verify(signed_id, purpose: combine_signed_id_purposes(purpose), **options)
74
92
  find(id)
75
93
  end
76
94
  end
77
95
 
78
- # The verifier instance that all signed ids are generated and verified from. By default, it'll be initialized
79
- # with the class-level +signed_id_verifier_secret+, which within Rails comes from
80
- # {Rails.application.key_generator}[rdoc-ref:Rails::Application#key_generator].
81
- # By default, it's SHA256 for the digest and JSON for the serialization.
82
96
  def signed_id_verifier
83
- @signed_id_verifier ||= begin
84
- secret = signed_id_verifier_secret
85
- secret = secret.call if secret.respond_to?(:call)
97
+ if signed_id_verifier_secret
98
+ @signed_id_verifier ||= begin
99
+ secret = signed_id_verifier_secret
100
+ secret = secret.call if secret.respond_to?(:call)
101
+
102
+ if secret.nil?
103
+ raise ArgumentError, "You must set ActiveRecord::Base.signed_id_verifier_secret to use signed IDs"
104
+ end
86
105
 
87
- if secret.nil?
88
- raise ArgumentError, "You must set ActiveRecord::Base.signed_id_verifier_secret to use signed ids"
89
- else
90
106
  ActiveSupport::MessageVerifier.new secret, digest: "SHA256", serializer: JSON, url_safe: true
91
107
  end
108
+ else
109
+ return _signed_id_verifier if _signed_id_verifier
110
+
111
+ if ActiveRecord.message_verifiers.nil?
112
+ raise "You must set ActiveRecord.message_verifiers to use signed IDs"
113
+ end
114
+
115
+ ActiveRecord.message_verifiers["active_record/signed_id"]
92
116
  end
93
117
  end
94
118
 
@@ -96,7 +120,11 @@ module ActiveRecord
96
120
  # verifiers for different classes. This is also helpful if you need to rotate keys, as you can prepare
97
121
  # your custom verifier for that in advance. See ActiveSupport::MessageVerifier for details.
98
122
  def signed_id_verifier=(verifier)
99
- @signed_id_verifier = verifier
123
+ if signed_id_verifier_secret
124
+ @signed_id_verifier = verifier
125
+ else
126
+ self._signed_id_verifier = verifier
127
+ end
100
128
  end
101
129
 
102
130
  # :nodoc:
@@ -31,8 +31,11 @@ module ActiveRecord
31
31
  class Substitute; end # :nodoc:
32
32
 
33
33
  class Query # :nodoc:
34
- def initialize(sql)
34
+ attr_reader :retryable
35
+
36
+ def initialize(sql, retryable:)
35
37
  @sql = sql
38
+ @retryable = retryable
36
39
  end
37
40
 
38
41
  def sql_for(binds, connection)
@@ -41,11 +44,12 @@ module ActiveRecord
41
44
  end
42
45
 
43
46
  class PartialQuery < Query # :nodoc:
44
- def initialize(values)
47
+ def initialize(values, retryable:)
45
48
  @values = values
46
49
  @indexes = values.each_with_index.find_all { |thing, i|
47
50
  Substitute === thing
48
51
  }.map(&:last)
52
+ @retryable = retryable
49
53
  end
50
54
 
51
55
  def sql_for(binds, connection)
@@ -94,12 +98,12 @@ module ActiveRecord
94
98
  end
95
99
  end
96
100
 
97
- def self.query(sql)
98
- Query.new(sql)
101
+ def self.query(...)
102
+ Query.new(...)
99
103
  end
100
104
 
101
- def self.partial_query(values)
102
- PartialQuery.new(values)
105
+ def self.partial_query(...)
106
+ PartialQuery.new(...)
103
107
  end
104
108
 
105
109
  def self.partial_query_collector
@@ -142,14 +146,14 @@ module ActiveRecord
142
146
  @model = model
143
147
  end
144
148
 
145
- def execute(params, connection, allow_retry: false, async: false, &block)
149
+ def execute(params, connection, async: false, &block)
146
150
  bind_values = @bind_map.bind params
147
151
  sql = @query_builder.sql_for bind_values, connection
148
152
 
149
153
  if async
150
- @model.async_find_by_sql(sql, bind_values, preparable: true, allow_retry: allow_retry, &block)
154
+ @model.async_find_by_sql(sql, bind_values, preparable: true, allow_retry: @query_builder.retryable, &block)
151
155
  else
152
- @model.find_by_sql(sql, bind_values, preparable: true, allow_retry: allow_retry, &block)
156
+ @model.find_by_sql(sql, bind_values, preparable: true, allow_retry: @query_builder.retryable, &block)
153
157
  end
154
158
  rescue ::RangeError
155
159
  async ? Promise.wrap([]) : []
@@ -146,37 +146,43 @@ module ActiveRecord
146
146
  define_method("#{accessor_key}_changed?") do
147
147
  return false unless attribute_changed?(store_attribute)
148
148
  prev_store, new_store = changes[store_attribute]
149
- prev_store&.dig(key) != new_store&.dig(key)
149
+ accessor = store_accessor_for(store_attribute)
150
+ accessor.get(prev_store, key) != accessor.get(new_store, key)
150
151
  end
151
152
 
152
153
  define_method("#{accessor_key}_change") do
153
154
  return unless attribute_changed?(store_attribute)
154
155
  prev_store, new_store = changes[store_attribute]
155
- [prev_store&.dig(key), new_store&.dig(key)]
156
+ accessor = store_accessor_for(store_attribute)
157
+ [accessor.get(prev_store, key), accessor.get(new_store, key)]
156
158
  end
157
159
 
158
160
  define_method("#{accessor_key}_was") do
159
161
  return unless attribute_changed?(store_attribute)
160
162
  prev_store, _new_store = changes[store_attribute]
161
- prev_store&.dig(key)
163
+ accessor = store_accessor_for(store_attribute)
164
+ accessor.get(prev_store, key)
162
165
  end
163
166
 
164
167
  define_method("saved_change_to_#{accessor_key}?") do
165
168
  return false unless saved_change_to_attribute?(store_attribute)
166
169
  prev_store, new_store = saved_changes[store_attribute]
167
- prev_store&.dig(key) != new_store&.dig(key)
170
+ accessor = store_accessor_for(store_attribute)
171
+ accessor.get(prev_store, key) != accessor.get(new_store, key)
168
172
  end
169
173
 
170
174
  define_method("saved_change_to_#{accessor_key}") do
171
175
  return unless saved_change_to_attribute?(store_attribute)
172
176
  prev_store, new_store = saved_changes[store_attribute]
173
- [prev_store&.dig(key), new_store&.dig(key)]
177
+ accessor = store_accessor_for(store_attribute)
178
+ [accessor.get(prev_store, key), accessor.get(new_store, key)]
174
179
  end
175
180
 
176
181
  define_method("#{accessor_key}_before_last_save") do
177
182
  return unless saved_change_to_attribute?(store_attribute)
178
183
  prev_store, _new_store = saved_changes[store_attribute]
179
- prev_store&.dig(key)
184
+ accessor = store_accessor_for(store_attribute)
185
+ accessor.get(prev_store, key)
180
186
  end
181
187
  end
182
188
  end
@@ -225,39 +231,58 @@ module ActiveRecord
225
231
  end
226
232
 
227
233
  class HashAccessor # :nodoc:
234
+ def self.get(store_object, key)
235
+ if store_object
236
+ store_object[key]
237
+ end
238
+ end
239
+
228
240
  def self.read(object, attribute, key)
229
- prepare(object, attribute)
230
- object.public_send(attribute)[key]
241
+ store_object = prepare(object, attribute)
242
+ store_object[key]
231
243
  end
232
244
 
233
245
  def self.write(object, attribute, key, value)
234
- prepare(object, attribute)
235
- object.public_send(attribute)[key] = value if value != read(object, attribute, key)
246
+ store_object = prepare(object, attribute)
247
+ store_object[key] = value if value != store_object[key]
236
248
  end
237
249
 
238
250
  def self.prepare(object, attribute)
239
- object.public_send :"#{attribute}=", {} unless object.send(attribute)
251
+ store_object = object.public_send(attribute)
252
+
253
+ if store_object.nil?
254
+ store_object = {}
255
+ object.public_send(:"#{attribute}=", store_object)
256
+ end
257
+
258
+ store_object
240
259
  end
241
260
  end
242
261
 
243
262
  class StringKeyedHashAccessor < HashAccessor # :nodoc:
263
+ def self.get(store_object, key)
264
+ super store_object, Symbol === key ? key.name : key.to_s
265
+ end
266
+
244
267
  def self.read(object, attribute, key)
245
- super object, attribute, key.to_s
268
+ super object, attribute, Symbol === key ? key.name : key.to_s
246
269
  end
247
270
 
248
271
  def self.write(object, attribute, key, value)
249
- super object, attribute, key.to_s, value
272
+ super object, attribute, Symbol === key ? key.name : key.to_s, value
250
273
  end
251
274
  end
252
275
 
253
276
  class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor # :nodoc:
254
- def self.prepare(object, store_attribute)
255
- attribute = object.send(store_attribute)
256
- unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess)
257
- attribute = IndifferentCoder.as_indifferent_hash(attribute)
258
- object.public_send :"#{store_attribute}=", attribute
277
+ def self.prepare(object, attribute)
278
+ store_object = object.public_send(attribute)
279
+
280
+ unless store_object.is_a?(ActiveSupport::HashWithIndifferentAccess)
281
+ store_object = IndifferentCoder.as_indifferent_hash(store_object)
282
+ object.public_send :"#{attribute}=", store_object
259
283
  end
260
- attribute
284
+
285
+ store_object
261
286
  end
262
287
  end
263
288
 
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Tasks # :nodoc:
5
+ class AbstractTasks # :nodoc:
6
+ def self.using_database_configurations?
7
+ true
8
+ end
9
+
10
+ def initialize(db_config)
11
+ @db_config = db_config
12
+ @configuration_hash = db_config.configuration_hash
13
+ end
14
+
15
+ def charset
16
+ connection.encoding
17
+ end
18
+
19
+ def collation
20
+ connection.collation
21
+ end
22
+
23
+ def check_current_protected_environment!(db_config, migration_class)
24
+ with_temporary_pool(db_config, migration_class) do |pool|
25
+ migration_context = pool.migration_context
26
+ current = migration_context.current_environment
27
+ stored = migration_context.last_stored_environment
28
+
29
+ if migration_context.protected_environment?
30
+ raise ActiveRecord::ProtectedEnvironmentError.new(stored)
31
+ end
32
+
33
+ if stored && stored != current
34
+ raise ActiveRecord::EnvironmentMismatchError.new(current: current, stored: stored)
35
+ end
36
+ rescue ActiveRecord::NoDatabaseError
37
+ end
38
+ end
39
+
40
+ private
41
+ attr_reader :db_config, :configuration_hash
42
+
43
+ def connection
44
+ ActiveRecord::Base.lease_connection
45
+ end
46
+
47
+ def establish_connection(config = db_config)
48
+ ActiveRecord::Base.establish_connection(config)
49
+ end
50
+
51
+ def configuration_hash_without_database
52
+ configuration_hash.merge(database: nil)
53
+ end
54
+
55
+ def run_cmd(cmd, *args, **opts)
56
+ fail run_cmd_error(cmd, args) unless Kernel.system(cmd, *args, opts)
57
+ end
58
+
59
+ def run_cmd_error(cmd, args)
60
+ msg = +"failed to execute:\n"
61
+ msg << "#{cmd} #{args.join(' ')}\n\n"
62
+ msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n"
63
+ msg
64
+ end
65
+
66
+ def with_temporary_pool(db_config, migration_class, clobber: false)
67
+ original_db_config = migration_class.connection_db_config
68
+ pool = migration_class.connection_handler.establish_connection(db_config, clobber: clobber)
69
+
70
+ yield pool
71
+ ensure
72
+ migration_class.connection_handler.establish_connection(original_db_config, clobber: clobber)
73
+ end
74
+ end
75
+ end
76
+ end