activerecord 7.0.8.7 → 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.
Files changed (227) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1339 -1572
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +15 -16
  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 +193 -97
  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 +40 -26
  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 +63 -43
  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 +289 -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 -108
  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 +17 -12
  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/range.rb +11 -2
  72. data/lib/active_record/connection_adapters/postgresql/quoting.rb +9 -6
  73. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +3 -9
  74. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +76 -6
  75. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +131 -2
  76. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +42 -0
  77. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +351 -54
  78. data/lib/active_record/connection_adapters/postgresql_adapter.rb +336 -168
  79. data/lib/active_record/connection_adapters/schema_cache.rb +287 -59
  80. data/lib/active_record/connection_adapters/sqlite3/column.rb +49 -0
  81. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +42 -36
  82. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +4 -3
  83. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +1 -0
  84. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +26 -7
  85. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +162 -77
  86. data/lib/active_record/connection_adapters/statement_pool.rb +7 -0
  87. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +98 -0
  88. data/lib/active_record/connection_adapters/trilogy_adapter.rb +254 -0
  89. data/lib/active_record/connection_adapters.rb +3 -1
  90. data/lib/active_record/connection_handling.rb +71 -94
  91. data/lib/active_record/core.rb +128 -138
  92. data/lib/active_record/counter_cache.rb +46 -25
  93. data/lib/active_record/database_configurations/database_config.rb +9 -3
  94. data/lib/active_record/database_configurations/hash_config.rb +22 -12
  95. data/lib/active_record/database_configurations/url_config.rb +17 -11
  96. data/lib/active_record/database_configurations.rb +86 -33
  97. data/lib/active_record/delegated_type.rb +8 -3
  98. data/lib/active_record/deprecator.rb +7 -0
  99. data/lib/active_record/destroy_association_async_job.rb +2 -0
  100. data/lib/active_record/encryption/auto_filtered_parameters.rb +66 -0
  101. data/lib/active_record/encryption/cipher/aes256_gcm.rb +4 -1
  102. data/lib/active_record/encryption/config.rb +25 -1
  103. data/lib/active_record/encryption/configurable.rb +12 -19
  104. data/lib/active_record/encryption/context.rb +10 -3
  105. data/lib/active_record/encryption/contexts.rb +5 -1
  106. data/lib/active_record/encryption/derived_secret_key_provider.rb +8 -2
  107. data/lib/active_record/encryption/encryptable_record.rb +36 -18
  108. data/lib/active_record/encryption/encrypted_attribute_type.rb +17 -6
  109. data/lib/active_record/encryption/extended_deterministic_queries.rb +66 -54
  110. data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +2 -2
  111. data/lib/active_record/encryption/key_generator.rb +12 -1
  112. data/lib/active_record/encryption/message_serializer.rb +2 -0
  113. data/lib/active_record/encryption/properties.rb +3 -3
  114. data/lib/active_record/encryption/scheme.rb +19 -22
  115. data/lib/active_record/encryption.rb +1 -0
  116. data/lib/active_record/enum.rb +113 -26
  117. data/lib/active_record/errors.rb +89 -15
  118. data/lib/active_record/explain.rb +23 -3
  119. data/lib/active_record/fixture_set/model_metadata.rb +14 -4
  120. data/lib/active_record/fixture_set/render_context.rb +2 -0
  121. data/lib/active_record/fixture_set/table_row.rb +29 -8
  122. data/lib/active_record/fixtures.rb +119 -71
  123. data/lib/active_record/future_result.rb +30 -5
  124. data/lib/active_record/gem_version.rb +4 -4
  125. data/lib/active_record/inheritance.rb +30 -16
  126. data/lib/active_record/insert_all.rb +55 -8
  127. data/lib/active_record/integration.rb +8 -8
  128. data/lib/active_record/internal_metadata.rb +118 -30
  129. data/lib/active_record/locking/pessimistic.rb +5 -2
  130. data/lib/active_record/log_subscriber.rb +29 -12
  131. data/lib/active_record/marshalling.rb +56 -0
  132. data/lib/active_record/message_pack.rb +124 -0
  133. data/lib/active_record/middleware/database_selector/resolver.rb +4 -0
  134. data/lib/active_record/middleware/database_selector.rb +5 -7
  135. data/lib/active_record/middleware/shard_selector.rb +3 -1
  136. data/lib/active_record/migration/command_recorder.rb +100 -4
  137. data/lib/active_record/migration/compatibility.rb +131 -5
  138. data/lib/active_record/migration/default_strategy.rb +23 -0
  139. data/lib/active_record/migration/execution_strategy.rb +19 -0
  140. data/lib/active_record/migration.rb +213 -109
  141. data/lib/active_record/model_schema.rb +47 -27
  142. data/lib/active_record/nested_attributes.rb +28 -3
  143. data/lib/active_record/normalization.rb +158 -0
  144. data/lib/active_record/persistence.rb +183 -33
  145. data/lib/active_record/promise.rb +84 -0
  146. data/lib/active_record/query_cache.rb +3 -21
  147. data/lib/active_record/query_logs.rb +77 -52
  148. data/lib/active_record/query_logs_formatter.rb +41 -0
  149. data/lib/active_record/querying.rb +15 -2
  150. data/lib/active_record/railtie.rb +107 -45
  151. data/lib/active_record/railties/controller_runtime.rb +10 -5
  152. data/lib/active_record/railties/databases.rake +139 -145
  153. data/lib/active_record/railties/job_runtime.rb +23 -0
  154. data/lib/active_record/readonly_attributes.rb +32 -5
  155. data/lib/active_record/reflection.rb +169 -45
  156. data/lib/active_record/relation/batches/batch_enumerator.rb +5 -3
  157. data/lib/active_record/relation/batches.rb +190 -61
  158. data/lib/active_record/relation/calculations.rb +152 -63
  159. data/lib/active_record/relation/delegation.rb +22 -8
  160. data/lib/active_record/relation/finder_methods.rb +85 -15
  161. data/lib/active_record/relation/merger.rb +2 -0
  162. data/lib/active_record/relation/predicate_builder/association_query_value.rb +11 -2
  163. data/lib/active_record/relation/predicate_builder/relation_handler.rb +5 -1
  164. data/lib/active_record/relation/predicate_builder.rb +26 -14
  165. data/lib/active_record/relation/query_attribute.rb +2 -1
  166. data/lib/active_record/relation/query_methods.rb +351 -62
  167. data/lib/active_record/relation/spawn_methods.rb +18 -1
  168. data/lib/active_record/relation.rb +76 -35
  169. data/lib/active_record/result.rb +19 -5
  170. data/lib/active_record/runtime_registry.rb +10 -1
  171. data/lib/active_record/sanitization.rb +51 -11
  172. data/lib/active_record/schema.rb +2 -3
  173. data/lib/active_record/schema_dumper.rb +41 -7
  174. data/lib/active_record/schema_migration.rb +68 -33
  175. data/lib/active_record/scoping/default.rb +15 -5
  176. data/lib/active_record/scoping/named.rb +2 -2
  177. data/lib/active_record/scoping.rb +2 -1
  178. data/lib/active_record/secure_password.rb +60 -0
  179. data/lib/active_record/secure_token.rb +21 -3
  180. data/lib/active_record/signed_id.rb +7 -5
  181. data/lib/active_record/store.rb +8 -8
  182. data/lib/active_record/suppressor.rb +3 -1
  183. data/lib/active_record/table_metadata.rb +10 -1
  184. data/lib/active_record/tasks/database_tasks.rb +127 -105
  185. data/lib/active_record/tasks/mysql_database_tasks.rb +15 -6
  186. data/lib/active_record/tasks/postgresql_database_tasks.rb +16 -13
  187. data/lib/active_record/tasks/sqlite_database_tasks.rb +14 -7
  188. data/lib/active_record/test_fixtures.rb +113 -96
  189. data/lib/active_record/timestamp.rb +26 -14
  190. data/lib/active_record/token_for.rb +113 -0
  191. data/lib/active_record/touch_later.rb +11 -6
  192. data/lib/active_record/transactions.rb +36 -10
  193. data/lib/active_record/type/adapter_specific_registry.rb +1 -8
  194. data/lib/active_record/type/internal/timezone.rb +7 -2
  195. data/lib/active_record/type/time.rb +4 -0
  196. data/lib/active_record/validations/absence.rb +1 -1
  197. data/lib/active_record/validations/numericality.rb +5 -4
  198. data/lib/active_record/validations/presence.rb +5 -28
  199. data/lib/active_record/validations/uniqueness.rb +47 -2
  200. data/lib/active_record/validations.rb +8 -4
  201. data/lib/active_record/version.rb +1 -1
  202. data/lib/active_record.rb +121 -16
  203. data/lib/arel/errors.rb +10 -0
  204. data/lib/arel/factory_methods.rb +4 -0
  205. data/lib/arel/nodes/binary.rb +6 -1
  206. data/lib/arel/nodes/bound_sql_literal.rb +61 -0
  207. data/lib/arel/nodes/cte.rb +36 -0
  208. data/lib/arel/nodes/fragments.rb +35 -0
  209. data/lib/arel/nodes/homogeneous_in.rb +0 -8
  210. data/lib/arel/nodes/leading_join.rb +8 -0
  211. data/lib/arel/nodes/node.rb +111 -2
  212. data/lib/arel/nodes/sql_literal.rb +6 -0
  213. data/lib/arel/nodes/table_alias.rb +4 -0
  214. data/lib/arel/nodes.rb +4 -0
  215. data/lib/arel/predications.rb +2 -0
  216. data/lib/arel/table.rb +9 -5
  217. data/lib/arel/visitors/mysql.rb +8 -1
  218. data/lib/arel/visitors/to_sql.rb +81 -17
  219. data/lib/arel/visitors/visitor.rb +2 -2
  220. data/lib/arel.rb +16 -2
  221. data/lib/rails/generators/active_record/application_record/USAGE +8 -0
  222. data/lib/rails/generators/active_record/migration.rb +3 -1
  223. data/lib/rails/generators/active_record/model/USAGE +113 -0
  224. data/lib/rails/generators/active_record/model/model_generator.rb +15 -6
  225. metadata +52 -17
  226. data/lib/active_record/connection_adapters/legacy_pool_manager.rb +0 -35
  227. 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
@@ -315,11 +323,7 @@ module ActiveRecord
315
323
  # The list of columns names the model should ignore. Ignored columns won't have attribute
316
324
  # accessors defined, and won't be referenced in SQL queries.
317
325
  def ignored_columns
318
- if defined?(@ignored_columns)
319
- @ignored_columns
320
- else
321
- superclass.ignored_columns
322
- end
326
+ @ignored_columns || superclass.ignored_columns
323
327
  end
324
328
 
325
329
  # Sets the columns names the model should ignore. Ignored columns won't have attribute
@@ -340,7 +344,7 @@ module ActiveRecord
340
344
  # # name :string, limit: 255
341
345
  # # category :string, limit: 255
342
346
  #
343
- # self.ignored_columns = [:category]
347
+ # self.ignored_columns += [:category]
344
348
  # end
345
349
  #
346
350
  # The schema still contains "category", but now the model omits it, so any meta-driven code or
@@ -425,6 +429,12 @@ module ActiveRecord
425
429
  @columns ||= columns_hash.values.freeze
426
430
  end
427
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
+
428
438
  def attribute_types # :nodoc:
429
439
  load_schema
430
440
  @attribute_types ||= Hash.new(Type.default_value)
@@ -515,7 +525,7 @@ module ActiveRecord
515
525
  # when just after creating a table you want to populate it with some default
516
526
  # values, e.g.:
517
527
  #
518
- # class CreateJobLevels < ActiveRecord::Migration[7.0]
528
+ # class CreateJobLevels < ActiveRecord::Migration[7.1]
519
529
  # def up
520
530
  # create_table :job_levels do |t|
521
531
  # t.integer :id
@@ -548,10 +558,36 @@ module ActiveRecord
548
558
  @load_schema_monitor = Monitor.new
549
559
  end
550
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
+
551
583
  private
552
584
  def inherited(child_class)
553
585
  super
554
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
555
591
  end
556
592
 
557
593
  def schema_loaded?
@@ -561,7 +597,7 @@ module ActiveRecord
561
597
  def load_schema
562
598
  return if schema_loaded?
563
599
  @load_schema_monitor.synchronize do
564
- return if defined?(@columns_hash) && @columns_hash
600
+ return if @columns_hash
565
601
 
566
602
  load_schema!
567
603
 
@@ -589,26 +625,10 @@ module ActiveRecord
589
625
  default: column.default,
590
626
  user_provided_default: false
591
627
  )
628
+ alias_attribute :id_value, :id if name == "id"
592
629
  end
593
- end
594
630
 
595
- def reload_schema_from_cache
596
- @arel_table = nil
597
- @column_names = nil
598
- @symbol_column_to_string_name_hash = nil
599
- @attribute_types = nil
600
- @content_columns = nil
601
- @default_attributes = nil
602
- @column_defaults = nil
603
- @attributes_builder = nil
604
- @columns = nil
605
- @columns_hash = nil
606
- @schema_loaded = false
607
- @attribute_names = nil
608
- @yaml_encoder = nil
609
- subclasses.each do |descendant|
610
- descendant.send(:reload_schema_from_cache)
611
- end
631
+ super
612
632
  end
613
633
 
614
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
 
@@ -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