activerecord 7.0.10 → 7.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.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (228) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1348 -1672
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +15 -17
  5. data/lib/active_record/aggregations.rb +16 -13
  6. data/lib/active_record/association_relation.rb +1 -1
  7. data/lib/active_record/associations/association.rb +18 -3
  8. data/lib/active_record/associations/association_scope.rb +16 -9
  9. data/lib/active_record/associations/belongs_to_association.rb +14 -6
  10. data/lib/active_record/associations/builder/association.rb +3 -3
  11. data/lib/active_record/associations/builder/belongs_to.rb +21 -8
  12. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +1 -5
  13. data/lib/active_record/associations/builder/singular_association.rb +4 -0
  14. data/lib/active_record/associations/collection_association.rb +17 -9
  15. data/lib/active_record/associations/collection_proxy.rb +16 -11
  16. data/lib/active_record/associations/foreign_association.rb +10 -3
  17. data/lib/active_record/associations/has_many_association.rb +20 -13
  18. data/lib/active_record/associations/has_many_through_association.rb +10 -6
  19. data/lib/active_record/associations/has_one_association.rb +10 -3
  20. data/lib/active_record/associations/join_dependency.rb +10 -8
  21. data/lib/active_record/associations/preloader/association.rb +27 -6
  22. data/lib/active_record/associations/preloader.rb +12 -9
  23. data/lib/active_record/associations/singular_association.rb +1 -1
  24. data/lib/active_record/associations/through_association.rb +22 -11
  25. data/lib/active_record/associations.rb +313 -217
  26. data/lib/active_record/attribute_assignment.rb +0 -2
  27. data/lib/active_record/attribute_methods/before_type_cast.rb +17 -0
  28. data/lib/active_record/attribute_methods/dirty.rb +48 -38
  29. data/lib/active_record/attribute_methods/primary_key.rb +76 -24
  30. data/lib/active_record/attribute_methods/query.rb +28 -16
  31. data/lib/active_record/attribute_methods/read.rb +18 -5
  32. data/lib/active_record/attribute_methods/serialization.rb +150 -31
  33. data/lib/active_record/attribute_methods/write.rb +3 -3
  34. data/lib/active_record/attribute_methods.rb +105 -21
  35. data/lib/active_record/attributes.rb +3 -3
  36. data/lib/active_record/autosave_association.rb +55 -9
  37. data/lib/active_record/base.rb +7 -2
  38. data/lib/active_record/callbacks.rb +10 -24
  39. data/lib/active_record/coders/column_serializer.rb +61 -0
  40. data/lib/active_record/coders/json.rb +1 -1
  41. data/lib/active_record/coders/yaml_column.rb +70 -42
  42. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +163 -88
  43. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +2 -0
  44. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +3 -1
  45. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +65 -49
  46. data/lib/active_record/connection_adapters/abstract/database_limits.rb +5 -0
  47. data/lib/active_record/connection_adapters/abstract/database_statements.rb +109 -32
  48. data/lib/active_record/connection_adapters/abstract/query_cache.rb +60 -22
  49. data/lib/active_record/connection_adapters/abstract/quoting.rb +41 -6
  50. data/lib/active_record/connection_adapters/abstract/savepoints.rb +4 -3
  51. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +18 -4
  52. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +137 -11
  53. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +291 -122
  54. data/lib/active_record/connection_adapters/abstract/transaction.rb +280 -58
  55. data/lib/active_record/connection_adapters/abstract_adapter.rb +502 -91
  56. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +200 -115
  57. data/lib/active_record/connection_adapters/column.rb +9 -0
  58. data/lib/active_record/connection_adapters/mysql/column.rb +1 -0
  59. data/lib/active_record/connection_adapters/mysql/database_statements.rb +22 -143
  60. data/lib/active_record/connection_adapters/mysql/quoting.rb +16 -12
  61. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +9 -0
  62. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +6 -0
  63. data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +1 -1
  64. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +18 -14
  65. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +148 -0
  66. data/lib/active_record/connection_adapters/mysql2_adapter.rb +98 -53
  67. data/lib/active_record/connection_adapters/pool_config.rb +14 -5
  68. data/lib/active_record/connection_adapters/pool_manager.rb +19 -9
  69. data/lib/active_record/connection_adapters/postgresql/column.rb +1 -2
  70. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +76 -29
  71. data/lib/active_record/connection_adapters/postgresql/oid/money.rb +2 -3
  72. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +11 -2
  73. data/lib/active_record/connection_adapters/postgresql/quoting.rb +9 -6
  74. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +3 -9
  75. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +76 -6
  76. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +131 -2
  77. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +42 -0
  78. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +352 -55
  79. data/lib/active_record/connection_adapters/postgresql_adapter.rb +336 -168
  80. data/lib/active_record/connection_adapters/schema_cache.rb +287 -59
  81. data/lib/active_record/connection_adapters/sqlite3/column.rb +49 -0
  82. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +42 -36
  83. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +4 -3
  84. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +1 -0
  85. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +26 -7
  86. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +162 -77
  87. data/lib/active_record/connection_adapters/statement_pool.rb +7 -0
  88. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +98 -0
  89. data/lib/active_record/connection_adapters/trilogy_adapter.rb +254 -0
  90. data/lib/active_record/connection_adapters.rb +3 -1
  91. data/lib/active_record/connection_handling.rb +72 -95
  92. data/lib/active_record/core.rb +131 -142
  93. data/lib/active_record/counter_cache.rb +46 -25
  94. data/lib/active_record/database_configurations/database_config.rb +9 -3
  95. data/lib/active_record/database_configurations/hash_config.rb +22 -12
  96. data/lib/active_record/database_configurations/url_config.rb +17 -11
  97. data/lib/active_record/database_configurations.rb +86 -33
  98. data/lib/active_record/delegated_type.rb +11 -6
  99. data/lib/active_record/deprecator.rb +7 -0
  100. data/lib/active_record/destroy_association_async_job.rb +2 -0
  101. data/lib/active_record/encryption/auto_filtered_parameters.rb +66 -0
  102. data/lib/active_record/encryption/cipher/aes256_gcm.rb +4 -1
  103. data/lib/active_record/encryption/config.rb +25 -1
  104. data/lib/active_record/encryption/configurable.rb +12 -19
  105. data/lib/active_record/encryption/context.rb +10 -3
  106. data/lib/active_record/encryption/contexts.rb +5 -1
  107. data/lib/active_record/encryption/derived_secret_key_provider.rb +8 -2
  108. data/lib/active_record/encryption/encryptable_record.rb +36 -18
  109. data/lib/active_record/encryption/encrypted_attribute_type.rb +17 -6
  110. data/lib/active_record/encryption/extended_deterministic_queries.rb +52 -12
  111. data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +2 -2
  112. data/lib/active_record/encryption/key_generator.rb +12 -1
  113. data/lib/active_record/encryption/message_serializer.rb +2 -0
  114. data/lib/active_record/encryption/properties.rb +3 -3
  115. data/lib/active_record/encryption/scheme.rb +19 -22
  116. data/lib/active_record/encryption.rb +1 -0
  117. data/lib/active_record/enum.rb +113 -26
  118. data/lib/active_record/errors.rb +89 -15
  119. data/lib/active_record/explain.rb +23 -3
  120. data/lib/active_record/fixture_set/model_metadata.rb +14 -4
  121. data/lib/active_record/fixture_set/render_context.rb +2 -0
  122. data/lib/active_record/fixture_set/table_row.rb +29 -8
  123. data/lib/active_record/fixtures.rb +119 -83
  124. data/lib/active_record/future_result.rb +30 -5
  125. data/lib/active_record/gem_version.rb +4 -4
  126. data/lib/active_record/inheritance.rb +30 -16
  127. data/lib/active_record/insert_all.rb +58 -11
  128. data/lib/active_record/integration.rb +8 -8
  129. data/lib/active_record/internal_metadata.rb +118 -30
  130. data/lib/active_record/locking/pessimistic.rb +5 -2
  131. data/lib/active_record/log_subscriber.rb +29 -12
  132. data/lib/active_record/marshalling.rb +56 -0
  133. data/lib/active_record/message_pack.rb +124 -0
  134. data/lib/active_record/middleware/database_selector/resolver.rb +4 -0
  135. data/lib/active_record/middleware/database_selector.rb +6 -8
  136. data/lib/active_record/middleware/shard_selector.rb +3 -1
  137. data/lib/active_record/migration/command_recorder.rb +100 -4
  138. data/lib/active_record/migration/compatibility.rb +131 -5
  139. data/lib/active_record/migration/default_strategy.rb +23 -0
  140. data/lib/active_record/migration/execution_strategy.rb +19 -0
  141. data/lib/active_record/migration.rb +215 -112
  142. data/lib/active_record/model_schema.rb +50 -32
  143. data/lib/active_record/nested_attributes.rb +31 -6
  144. data/lib/active_record/normalization.rb +158 -0
  145. data/lib/active_record/persistence.rb +183 -33
  146. data/lib/active_record/promise.rb +84 -0
  147. data/lib/active_record/query_cache.rb +3 -21
  148. data/lib/active_record/query_logs.rb +77 -52
  149. data/lib/active_record/query_logs_formatter.rb +41 -0
  150. data/lib/active_record/querying.rb +15 -2
  151. data/lib/active_record/railtie.rb +108 -46
  152. data/lib/active_record/railties/controller_runtime.rb +10 -5
  153. data/lib/active_record/railties/databases.rake +139 -145
  154. data/lib/active_record/railties/job_runtime.rb +23 -0
  155. data/lib/active_record/readonly_attributes.rb +32 -5
  156. data/lib/active_record/reflection.rb +169 -45
  157. data/lib/active_record/relation/batches/batch_enumerator.rb +5 -3
  158. data/lib/active_record/relation/batches.rb +190 -61
  159. data/lib/active_record/relation/calculations.rb +152 -63
  160. data/lib/active_record/relation/delegation.rb +23 -9
  161. data/lib/active_record/relation/finder_methods.rb +85 -15
  162. data/lib/active_record/relation/merger.rb +2 -0
  163. data/lib/active_record/relation/predicate_builder/association_query_value.rb +11 -2
  164. data/lib/active_record/relation/predicate_builder/relation_handler.rb +5 -1
  165. data/lib/active_record/relation/predicate_builder.rb +26 -14
  166. data/lib/active_record/relation/query_attribute.rb +2 -1
  167. data/lib/active_record/relation/query_methods.rb +351 -62
  168. data/lib/active_record/relation/spawn_methods.rb +18 -1
  169. data/lib/active_record/relation.rb +75 -34
  170. data/lib/active_record/result.rb +19 -5
  171. data/lib/active_record/runtime_registry.rb +10 -1
  172. data/lib/active_record/sanitization.rb +51 -11
  173. data/lib/active_record/schema.rb +2 -3
  174. data/lib/active_record/schema_dumper.rb +41 -7
  175. data/lib/active_record/schema_migration.rb +68 -33
  176. data/lib/active_record/scoping/default.rb +15 -5
  177. data/lib/active_record/scoping/named.rb +2 -2
  178. data/lib/active_record/scoping.rb +2 -1
  179. data/lib/active_record/secure_password.rb +60 -0
  180. data/lib/active_record/secure_token.rb +21 -3
  181. data/lib/active_record/signed_id.rb +7 -5
  182. data/lib/active_record/store.rb +8 -8
  183. data/lib/active_record/suppressor.rb +3 -1
  184. data/lib/active_record/table_metadata.rb +10 -1
  185. data/lib/active_record/tasks/database_tasks.rb +127 -105
  186. data/lib/active_record/tasks/mysql_database_tasks.rb +15 -6
  187. data/lib/active_record/tasks/postgresql_database_tasks.rb +16 -13
  188. data/lib/active_record/tasks/sqlite_database_tasks.rb +14 -7
  189. data/lib/active_record/test_fixtures.rb +113 -96
  190. data/lib/active_record/timestamp.rb +27 -15
  191. data/lib/active_record/token_for.rb +113 -0
  192. data/lib/active_record/touch_later.rb +11 -6
  193. data/lib/active_record/transactions.rb +36 -10
  194. data/lib/active_record/type/adapter_specific_registry.rb +1 -8
  195. data/lib/active_record/type/internal/timezone.rb +7 -2
  196. data/lib/active_record/type/time.rb +4 -0
  197. data/lib/active_record/validations/absence.rb +1 -1
  198. data/lib/active_record/validations/numericality.rb +5 -4
  199. data/lib/active_record/validations/presence.rb +5 -28
  200. data/lib/active_record/validations/uniqueness.rb +47 -2
  201. data/lib/active_record/validations.rb +8 -4
  202. data/lib/active_record/version.rb +1 -1
  203. data/lib/active_record.rb +121 -16
  204. data/lib/arel/errors.rb +10 -0
  205. data/lib/arel/factory_methods.rb +4 -0
  206. data/lib/arel/nodes/binary.rb +6 -1
  207. data/lib/arel/nodes/bound_sql_literal.rb +61 -0
  208. data/lib/arel/nodes/cte.rb +36 -0
  209. data/lib/arel/nodes/fragments.rb +35 -0
  210. data/lib/arel/nodes/homogeneous_in.rb +1 -9
  211. data/lib/arel/nodes/leading_join.rb +8 -0
  212. data/lib/arel/nodes/node.rb +111 -2
  213. data/lib/arel/nodes/sql_literal.rb +6 -0
  214. data/lib/arel/nodes/table_alias.rb +4 -0
  215. data/lib/arel/nodes.rb +4 -0
  216. data/lib/arel/predications.rb +2 -0
  217. data/lib/arel/table.rb +9 -5
  218. data/lib/arel/visitors/mysql.rb +8 -1
  219. data/lib/arel/visitors/to_sql.rb +81 -17
  220. data/lib/arel/visitors/visitor.rb +2 -2
  221. data/lib/arel.rb +16 -2
  222. data/lib/rails/generators/active_record/application_record/USAGE +8 -0
  223. data/lib/rails/generators/active_record/migration.rb +3 -1
  224. data/lib/rails/generators/active_record/model/USAGE +113 -0
  225. data/lib/rails/generators/active_record/model/model_generator.rb +15 -6
  226. metadata +52 -14
  227. data/lib/active_record/connection_adapters/legacy_pool_manager.rb +0 -35
  228. data/lib/active_record/null_relation.rb +0 -63
@@ -6,6 +6,13 @@ module ActiveRecord
6
6
  module ModelSchema
7
7
  extend ActiveSupport::Concern
8
8
 
9
+ ##
10
+ # :method: id_value
11
+ # :call-seq: id_value
12
+ #
13
+ # Returns the underlying column value for a column named "id". Useful when defining
14
+ # a composite primary key including an "id" column so that the value is readable.
15
+
9
16
  ##
10
17
  # :singleton-method: primary_key_prefix_type
11
18
  # :call-seq: primary_key_prefix_type
@@ -180,8 +187,9 @@ module ActiveRecord
180
187
  # artists, records => artists_records
181
188
  # records, artists => artists_records
182
189
  # music_artists, music_records => music_artists_records
190
+ # music.artists, music.records => music.artists_records
183
191
  def self.derive_join_table_name(first_table, second_table) # :nodoc:
184
- [first_table.to_s, second_table.to_s].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_")
192
+ [first_table.to_s, second_table.to_s].sort.join("\0").gsub(/^(.*[_.])(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_")
185
193
  end
186
194
 
187
195
  module ClassMethods
@@ -276,10 +284,8 @@ module ActiveRecord
276
284
 
277
285
  # Computes the table name, (re)sets it internally, and returns it.
278
286
  def reset_table_name # :nodoc:
279
- self.table_name = if self == Base
280
- nil
281
- elsif abstract_class?
282
- superclass.table_name
287
+ self.table_name = if abstract_class?
288
+ superclass == Base ? nil : superclass.table_name
283
289
  elsif superclass.abstract_class?
284
290
  superclass.table_name || compute_table_name
285
291
  else
@@ -317,11 +323,7 @@ module ActiveRecord
317
323
  # The list of columns names the model should ignore. Ignored columns won't have attribute
318
324
  # accessors defined, and won't be referenced in SQL queries.
319
325
  def ignored_columns
320
- if defined?(@ignored_columns)
321
- @ignored_columns
322
- else
323
- superclass.ignored_columns
324
- end
326
+ @ignored_columns || superclass.ignored_columns
325
327
  end
326
328
 
327
329
  # Sets the columns names the model should ignore. Ignored columns won't have attribute
@@ -342,7 +344,7 @@ module ActiveRecord
342
344
  # # name :string, limit: 255
343
345
  # # category :string, limit: 255
344
346
  #
345
- # self.ignored_columns = [:category]
347
+ # self.ignored_columns += [:category]
346
348
  # end
347
349
  #
348
350
  # The schema still contains "category", but now the model omits it, so any meta-driven code or
@@ -427,6 +429,12 @@ module ActiveRecord
427
429
  @columns ||= columns_hash.values.freeze
428
430
  end
429
431
 
432
+ def _returning_columns_for_insert # :nodoc:
433
+ @_returning_columns_for_insert ||= columns.filter_map do |c|
434
+ c.name if connection.return_value_after_insert?(c)
435
+ end
436
+ end
437
+
430
438
  def attribute_types # :nodoc:
431
439
  load_schema
432
440
  @attribute_types ||= Hash.new(Type.default_value)
@@ -459,7 +467,7 @@ module ActiveRecord
459
467
  end
460
468
 
461
469
  # Returns the column object for the named attribute.
462
- # Returns an ActiveRecord::ConnectionAdapters::NullColumn if the
470
+ # Returns an +ActiveRecord::ConnectionAdapters::NullColumn+ if the
463
471
  # named attribute does not exist.
464
472
  #
465
473
  # class Person < ActiveRecord::Base
@@ -517,7 +525,7 @@ module ActiveRecord
517
525
  # when just after creating a table you want to populate it with some default
518
526
  # values, e.g.:
519
527
  #
520
- # class CreateJobLevels < ActiveRecord::Migration[7.0]
528
+ # class CreateJobLevels < ActiveRecord::Migration[7.1]
521
529
  # def up
522
530
  # create_table :job_levels do |t|
523
531
  # t.integer :id
@@ -550,10 +558,36 @@ module ActiveRecord
550
558
  @load_schema_monitor = Monitor.new
551
559
  end
552
560
 
561
+ def reload_schema_from_cache(recursive = true)
562
+ @_returning_columns_for_insert = nil
563
+ @arel_table = nil
564
+ @column_names = nil
565
+ @symbol_column_to_string_name_hash = nil
566
+ @attribute_types = nil
567
+ @content_columns = nil
568
+ @default_attributes = nil
569
+ @column_defaults = nil
570
+ @attributes_builder = nil
571
+ @columns = nil
572
+ @columns_hash = nil
573
+ @schema_loaded = false
574
+ @attribute_names = nil
575
+ @yaml_encoder = nil
576
+ if recursive
577
+ subclasses.each do |descendant|
578
+ descendant.send(:reload_schema_from_cache)
579
+ end
580
+ end
581
+ end
582
+
553
583
  private
554
584
  def inherited(child_class)
555
585
  super
556
586
  child_class.initialize_load_schema_monitor
587
+ child_class.reload_schema_from_cache(false)
588
+ child_class.class_eval do
589
+ @ignored_columns = nil
590
+ end
557
591
  end
558
592
 
559
593
  def schema_loaded?
@@ -563,7 +597,7 @@ module ActiveRecord
563
597
  def load_schema
564
598
  return if schema_loaded?
565
599
  @load_schema_monitor.synchronize do
566
- return if defined?(@columns_hash) && @columns_hash
600
+ return if @columns_hash
567
601
 
568
602
  load_schema!
569
603
 
@@ -591,26 +625,10 @@ module ActiveRecord
591
625
  default: column.default,
592
626
  user_provided_default: false
593
627
  )
628
+ alias_attribute :id_value, :id if name == "id"
594
629
  end
595
- end
596
630
 
597
- def reload_schema_from_cache
598
- @arel_table = nil
599
- @column_names = nil
600
- @symbol_column_to_string_name_hash = nil
601
- @attribute_types = nil
602
- @content_columns = nil
603
- @default_attributes = nil
604
- @column_defaults = nil
605
- @attributes_builder = nil
606
- @columns = nil
607
- @columns_hash = nil
608
- @schema_loaded = false
609
- @attribute_names = nil
610
- @yaml_encoder = nil
611
- subclasses.each do |descendant|
612
- descendant.send(:reload_schema_from_cache)
613
- end
631
+ super
614
632
  end
615
633
 
616
634
  # Guesses the table name, but does not decorate it with prefix and suffix information.
@@ -15,7 +15,7 @@ module ActiveRecord
15
15
  class_attribute :nested_attributes_options, instance_writer: false, default: {}
16
16
  end
17
17
 
18
- # = Active Record Nested Attributes
18
+ # = Active Record Nested \Attributes
19
19
  #
20
20
  # Nested attributes allow you to save attributes on associated records
21
21
  # through the parent. By default nested attribute updating is turned off
@@ -280,6 +280,26 @@ module ActiveRecord
280
280
  # member = Member.new
281
281
  # member.avatar_attributes = {icon: 'sad'}
282
282
  # member.avatar.width # => 200
283
+ #
284
+ # === Creating forms with nested attributes
285
+ #
286
+ # Use ActionView::Helpers::FormHelper#fields_for to create form elements
287
+ # for updating or destroying nested attributes.
288
+ #
289
+ # === Testing
290
+ #
291
+ # If you are using ActionView::Helpers::FormHelper#fields_for, your integration
292
+ # tests should replicate the HTML structure it provides. For example;
293
+ #
294
+ # post members_path, params: {
295
+ # member: {
296
+ # name: 'joe',
297
+ # posts_attributes: {
298
+ # '0' => { title: 'Foo' },
299
+ # '1' => { title: 'Bar' }
300
+ # }
301
+ # }
302
+ # }
283
303
  module ClassMethods
284
304
  REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == "_destroy" || value.blank? } }
285
305
 
@@ -289,7 +309,7 @@ module ActiveRecord
289
309
  # [:allow_destroy]
290
310
  # If true, destroys any members from the attributes hash with a
291
311
  # <tt>_destroy</tt> key and a value that evaluates to +true+
292
- # (e.g. 1, '1', true, or 'true'). This option is false by default.
312
+ # (e.g. 1, '1', true, or 'true'). This option is off by default.
293
313
  # [:reject_if]
294
314
  # Allows you to specify a Proc or a Symbol pointing to a method
295
315
  # that checks whether a record should be built for a certain attribute
@@ -314,11 +334,11 @@ module ActiveRecord
314
334
  # nested attributes are going to be used when an associated record already
315
335
  # exists. In general, an existing record may either be updated with the
316
336
  # new set of attribute values or be replaced by a wholly new record
317
- # containing those values. By default the +:update_only+ option is false
337
+ # containing those values. By default the +:update_only+ option is +false+
318
338
  # and the nested attributes are used to update the existing record only
319
339
  # if they include the record's <tt>:id</tt> value. Otherwise a new
320
340
  # record will be instantiated and used to replace the existing one.
321
- # However if the +:update_only+ option is true, the nested attributes
341
+ # However if the +:update_only+ option is +true+, the nested attributes
322
342
  # are used to update the record's attributes always, regardless of
323
343
  # whether the <tt>:id</tt> is present. The option is ignored for collection
324
344
  # associations.
@@ -335,12 +355,17 @@ module ActiveRecord
335
355
  options.update(attr_names.extract_options!)
336
356
  options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only)
337
357
  options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank
358
+ options[:class] = self
338
359
 
339
360
  attr_names.each do |association_name|
340
361
  if reflection = _reflect_on_association(association_name)
341
362
  reflection.autosave = true
342
363
  define_autosave_validation_callbacks(reflection)
343
364
 
365
+ if nested_attributes_options.dig(association_name.to_sym, :class) == self
366
+ raise ArgumentError, "Already declared #{association_name} as an accepts_nested_attributes association for this class."
367
+ end
368
+
344
369
  nested_attributes_options = self.nested_attributes_options.dup
345
370
  nested_attributes_options[association_name.to_sym] = options
346
371
  self.nested_attributes_options = nested_attributes_options
@@ -375,11 +400,11 @@ module ActiveRecord
375
400
  end
376
401
  end
377
402
 
378
- # Returns ActiveRecord::AutosaveAssociation::marked_for_destruction? It's
403
+ # Returns ActiveRecord::AutosaveAssociation#marked_for_destruction? It's
379
404
  # used in conjunction with fields_for to build a form element for the
380
405
  # destruction of this association.
381
406
  #
382
- # See ActionView::Helpers::FormHelper::fields_for for more info.
407
+ # See ActionView::Helpers::FormHelper#fields_for for more info.
383
408
  def _destroy
384
409
  marked_for_destruction?
385
410
  end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord # :nodoc:
4
+ module Normalization
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ class_attribute :normalized_attributes, default: Set.new
9
+
10
+ before_validation :normalize_changed_in_place_attributes
11
+ end
12
+
13
+ # Normalizes a specified attribute using its declared normalizations.
14
+ #
15
+ # ==== Examples
16
+ #
17
+ # class User < ActiveRecord::Base
18
+ # normalizes :email, with: -> email { email.strip.downcase }
19
+ # end
20
+ #
21
+ # legacy_user = User.find(1)
22
+ # legacy_user.email # => " CRUISE-CONTROL@EXAMPLE.COM\n"
23
+ # legacy_user.normalize_attribute(:email)
24
+ # legacy_user.email # => "cruise-control@example.com"
25
+ # legacy_user.save
26
+ def normalize_attribute(name)
27
+ # Treat the value as a new, unnormalized value.
28
+ self[name] = self[name]
29
+ end
30
+
31
+ module ClassMethods
32
+ # Declares a normalization for one or more attributes. The normalization
33
+ # is applied when the attribute is assigned or updated, and the normalized
34
+ # value will be persisted to the database. The normalization is also
35
+ # applied to the corresponding keyword argument of query methods. This
36
+ # allows a record to be created and later queried using unnormalized
37
+ # values.
38
+ #
39
+ # However, to prevent confusion, the normalization will not be applied
40
+ # when the attribute is fetched from the database. This means that if a
41
+ # record was persisted before the normalization was declared, the record's
42
+ # attribute will not be normalized until either it is assigned a new
43
+ # value, or it is explicitly migrated via Normalization#normalize_attribute.
44
+ #
45
+ # Because the normalization may be applied multiple times, it should be
46
+ # _idempotent_. In other words, applying the normalization more than once
47
+ # should have the same result as applying it only once.
48
+ #
49
+ # By default, the normalization will not be applied to +nil+ values. This
50
+ # behavior can be changed with the +:apply_to_nil+ option.
51
+ #
52
+ # ==== Options
53
+ #
54
+ # * +:with+ - The normalization to apply.
55
+ # * +:apply_to_nil+ - Whether to apply the normalization to +nil+ values.
56
+ # Defaults to +false+.
57
+ #
58
+ # ==== Examples
59
+ #
60
+ # class User < ActiveRecord::Base
61
+ # normalizes :email, with: -> email { email.strip.downcase }
62
+ # normalizes :phone, with: -> phone { phone.delete("^0-9").delete_prefix("1") }
63
+ # end
64
+ #
65
+ # user = User.create(email: " CRUISE-CONTROL@EXAMPLE.COM\n")
66
+ # user.email # => "cruise-control@example.com"
67
+ #
68
+ # user = User.find_by(email: "\tCRUISE-CONTROL@EXAMPLE.COM ")
69
+ # user.email # => "cruise-control@example.com"
70
+ # user.email_before_type_cast # => "cruise-control@example.com"
71
+ #
72
+ # User.where(email: "\tCRUISE-CONTROL@EXAMPLE.COM ").count # => 1
73
+ # User.where(["email = ?", "\tCRUISE-CONTROL@EXAMPLE.COM "]).count # => 0
74
+ #
75
+ # User.exists?(email: "\tCRUISE-CONTROL@EXAMPLE.COM ") # => true
76
+ # User.exists?(["email = ?", "\tCRUISE-CONTROL@EXAMPLE.COM "]) # => false
77
+ #
78
+ # User.normalize_value_for(:phone, "+1 (555) 867-5309") # => "5558675309"
79
+ def normalizes(*names, with:, apply_to_nil: false)
80
+ names.each do |name|
81
+ attribute(name) do |cast_type|
82
+ NormalizedValueType.new(cast_type: cast_type, normalizer: with, normalize_nil: apply_to_nil)
83
+ end
84
+ end
85
+
86
+ self.normalized_attributes += names.map(&:to_sym)
87
+ end
88
+
89
+ # Normalizes a given +value+ using normalizations declared for +name+.
90
+ #
91
+ # ==== Examples
92
+ #
93
+ # class User < ActiveRecord::Base
94
+ # normalizes :email, with: -> email { email.strip.downcase }
95
+ # end
96
+ #
97
+ # User.normalize_value_for(:email, " CRUISE-CONTROL@EXAMPLE.COM\n")
98
+ # # => "cruise-control@example.com"
99
+ def normalize_value_for(name, value)
100
+ type_for_attribute(name).cast(value)
101
+ end
102
+ end
103
+
104
+ private
105
+ def normalize_changed_in_place_attributes
106
+ self.class.normalized_attributes.each do |name|
107
+ normalize_attribute(name) if attribute_changed_in_place?(name)
108
+ end
109
+ end
110
+
111
+ class NormalizedValueType < DelegateClass(ActiveModel::Type::Value) # :nodoc:
112
+ include ActiveModel::Type::SerializeCastValue
113
+
114
+ attr_reader :cast_type, :normalizer, :normalize_nil
115
+ alias :normalize_nil? :normalize_nil
116
+
117
+ def initialize(cast_type:, normalizer:, normalize_nil:)
118
+ @cast_type = cast_type
119
+ @normalizer = normalizer
120
+ @normalize_nil = normalize_nil
121
+ super(cast_type)
122
+ end
123
+
124
+ def cast(value)
125
+ normalize(super(value))
126
+ end
127
+
128
+ def serialize(value)
129
+ serialize_cast_value(cast(value))
130
+ end
131
+
132
+ def serialize_cast_value(value)
133
+ ActiveModel::Type::SerializeCastValue.serialize(cast_type, value)
134
+ end
135
+
136
+ def ==(other)
137
+ self.class == other.class &&
138
+ normalize_nil? == other.normalize_nil? &&
139
+ normalizer == other.normalizer &&
140
+ cast_type == other.cast_type
141
+ end
142
+ alias eql? ==
143
+
144
+ def hash
145
+ [self.class, cast_type, normalizer, normalize_nil?].hash
146
+ end
147
+
148
+ def inspect
149
+ Kernel.instance_method(:inspect).bind_call(self)
150
+ end
151
+
152
+ private
153
+ def normalize(value)
154
+ normalizer.call(value) unless value.nil? && !normalize_nil?
155
+ end
156
+ end
157
+ end
158
+ end