activerecord 7.0.4 → 7.1.5.1

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 (246) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1971 -1243
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +18 -18
  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 +20 -4
  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 +20 -14
  15. data/lib/active_record/associations/collection_proxy.rb +20 -10
  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/join_association.rb +3 -2
  21. data/lib/active_record/associations/join_dependency.rb +10 -10
  22. data/lib/active_record/associations/preloader/association.rb +31 -7
  23. data/lib/active_record/associations/preloader/through_association.rb +1 -1
  24. data/lib/active_record/associations/preloader.rb +13 -10
  25. data/lib/active_record/associations/singular_association.rb +1 -1
  26. data/lib/active_record/associations/through_association.rb +22 -11
  27. data/lib/active_record/associations.rb +333 -222
  28. data/lib/active_record/attribute_assignment.rb +0 -2
  29. data/lib/active_record/attribute_methods/before_type_cast.rb +17 -0
  30. data/lib/active_record/attribute_methods/dirty.rb +53 -35
  31. data/lib/active_record/attribute_methods/primary_key.rb +76 -24
  32. data/lib/active_record/attribute_methods/query.rb +28 -16
  33. data/lib/active_record/attribute_methods/read.rb +21 -8
  34. data/lib/active_record/attribute_methods/serialization.rb +150 -31
  35. data/lib/active_record/attribute_methods/time_zone_conversion.rb +4 -4
  36. data/lib/active_record/attribute_methods/write.rb +6 -6
  37. data/lib/active_record/attribute_methods.rb +148 -26
  38. data/lib/active_record/attributes.rb +3 -3
  39. data/lib/active_record/autosave_association.rb +59 -10
  40. data/lib/active_record/base.rb +7 -2
  41. data/lib/active_record/callbacks.rb +16 -32
  42. data/lib/active_record/coders/column_serializer.rb +61 -0
  43. data/lib/active_record/coders/json.rb +1 -1
  44. data/lib/active_record/coders/yaml_column.rb +70 -42
  45. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +163 -88
  46. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +2 -0
  47. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +3 -1
  48. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +80 -50
  49. data/lib/active_record/connection_adapters/abstract/database_limits.rb +5 -0
  50. data/lib/active_record/connection_adapters/abstract/database_statements.rb +129 -31
  51. data/lib/active_record/connection_adapters/abstract/query_cache.rb +62 -23
  52. data/lib/active_record/connection_adapters/abstract/quoting.rb +51 -7
  53. data/lib/active_record/connection_adapters/abstract/savepoints.rb +4 -3
  54. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +18 -4
  55. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +155 -25
  56. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +297 -127
  57. data/lib/active_record/connection_adapters/abstract/transaction.rb +287 -58
  58. data/lib/active_record/connection_adapters/abstract_adapter.rb +509 -103
  59. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +254 -125
  60. data/lib/active_record/connection_adapters/column.rb +9 -0
  61. data/lib/active_record/connection_adapters/mysql/column.rb +1 -0
  62. data/lib/active_record/connection_adapters/mysql/database_statements.rb +23 -144
  63. data/lib/active_record/connection_adapters/mysql/quoting.rb +29 -14
  64. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +9 -0
  65. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +10 -1
  66. data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +1 -1
  67. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +19 -13
  68. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +151 -0
  69. data/lib/active_record/connection_adapters/mysql2_adapter.rb +106 -55
  70. data/lib/active_record/connection_adapters/pool_config.rb +14 -5
  71. data/lib/active_record/connection_adapters/pool_manager.rb +19 -9
  72. data/lib/active_record/connection_adapters/postgresql/column.rb +16 -3
  73. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +75 -45
  74. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +1 -1
  75. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +6 -0
  76. data/lib/active_record/connection_adapters/postgresql/oid/money.rb +3 -2
  77. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +11 -2
  78. data/lib/active_record/connection_adapters/postgresql/oid/timestamp_with_time_zone.rb +2 -2
  79. data/lib/active_record/connection_adapters/postgresql/quoting.rb +41 -8
  80. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +3 -9
  81. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +76 -6
  82. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +131 -2
  83. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +53 -0
  84. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +365 -61
  85. data/lib/active_record/connection_adapters/postgresql/utils.rb +9 -10
  86. data/lib/active_record/connection_adapters/postgresql_adapter.rb +354 -193
  87. data/lib/active_record/connection_adapters/schema_cache.rb +287 -59
  88. data/lib/active_record/connection_adapters/sqlite3/column.rb +49 -0
  89. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +52 -39
  90. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +9 -5
  91. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +7 -0
  92. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +28 -9
  93. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +213 -85
  94. data/lib/active_record/connection_adapters/statement_pool.rb +7 -0
  95. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +99 -0
  96. data/lib/active_record/connection_adapters/trilogy_adapter.rb +258 -0
  97. data/lib/active_record/connection_adapters.rb +3 -1
  98. data/lib/active_record/connection_handling.rb +72 -95
  99. data/lib/active_record/core.rb +181 -154
  100. data/lib/active_record/counter_cache.rb +52 -27
  101. data/lib/active_record/database_configurations/connection_url_resolver.rb +1 -1
  102. data/lib/active_record/database_configurations/database_config.rb +9 -3
  103. data/lib/active_record/database_configurations/hash_config.rb +28 -14
  104. data/lib/active_record/database_configurations/url_config.rb +17 -11
  105. data/lib/active_record/database_configurations.rb +86 -33
  106. data/lib/active_record/delegated_type.rb +15 -10
  107. data/lib/active_record/deprecator.rb +7 -0
  108. data/lib/active_record/destroy_association_async_job.rb +3 -1
  109. data/lib/active_record/disable_joins_association_relation.rb +1 -1
  110. data/lib/active_record/encryption/auto_filtered_parameters.rb +66 -0
  111. data/lib/active_record/encryption/cipher/aes256_gcm.rb +4 -1
  112. data/lib/active_record/encryption/config.rb +25 -1
  113. data/lib/active_record/encryption/configurable.rb +12 -19
  114. data/lib/active_record/encryption/context.rb +10 -3
  115. data/lib/active_record/encryption/contexts.rb +5 -1
  116. data/lib/active_record/encryption/derived_secret_key_provider.rb +8 -2
  117. data/lib/active_record/encryption/encryptable_record.rb +42 -18
  118. data/lib/active_record/encryption/encrypted_attribute_type.rb +23 -8
  119. data/lib/active_record/encryption/extended_deterministic_queries.rb +66 -69
  120. data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +3 -3
  121. data/lib/active_record/encryption/key_generator.rb +12 -1
  122. data/lib/active_record/encryption/message_serializer.rb +2 -0
  123. data/lib/active_record/encryption/properties.rb +3 -3
  124. data/lib/active_record/encryption/scheme.rb +22 -21
  125. data/lib/active_record/encryption.rb +3 -0
  126. data/lib/active_record/enum.rb +112 -28
  127. data/lib/active_record/errors.rb +112 -18
  128. data/lib/active_record/explain.rb +23 -3
  129. data/lib/active_record/explain_subscriber.rb +1 -1
  130. data/lib/active_record/fixture_set/model_metadata.rb +14 -4
  131. data/lib/active_record/fixture_set/render_context.rb +2 -0
  132. data/lib/active_record/fixture_set/table_row.rb +29 -8
  133. data/lib/active_record/fixtures.rb +135 -71
  134. data/lib/active_record/future_result.rb +40 -5
  135. data/lib/active_record/gem_version.rb +4 -4
  136. data/lib/active_record/inheritance.rb +30 -16
  137. data/lib/active_record/insert_all.rb +57 -10
  138. data/lib/active_record/integration.rb +8 -8
  139. data/lib/active_record/internal_metadata.rb +120 -30
  140. data/lib/active_record/locking/optimistic.rb +33 -19
  141. data/lib/active_record/locking/pessimistic.rb +5 -2
  142. data/lib/active_record/log_subscriber.rb +29 -12
  143. data/lib/active_record/marshalling.rb +59 -0
  144. data/lib/active_record/message_pack.rb +124 -0
  145. data/lib/active_record/middleware/database_selector/resolver.rb +4 -0
  146. data/lib/active_record/middleware/database_selector.rb +9 -11
  147. data/lib/active_record/middleware/shard_selector.rb +3 -1
  148. data/lib/active_record/migration/command_recorder.rb +105 -7
  149. data/lib/active_record/migration/compatibility.rb +163 -58
  150. data/lib/active_record/migration/default_strategy.rb +23 -0
  151. data/lib/active_record/migration/execution_strategy.rb +19 -0
  152. data/lib/active_record/migration/pending_migration_connection.rb +21 -0
  153. data/lib/active_record/migration.rb +271 -114
  154. data/lib/active_record/model_schema.rb +69 -44
  155. data/lib/active_record/nested_attributes.rb +37 -8
  156. data/lib/active_record/normalization.rb +167 -0
  157. data/lib/active_record/persistence.rb +195 -42
  158. data/lib/active_record/promise.rb +84 -0
  159. data/lib/active_record/query_cache.rb +4 -22
  160. data/lib/active_record/query_logs.rb +87 -51
  161. data/lib/active_record/query_logs_formatter.rb +41 -0
  162. data/lib/active_record/querying.rb +15 -2
  163. data/lib/active_record/railtie.rb +107 -45
  164. data/lib/active_record/railties/controller_runtime.rb +14 -9
  165. data/lib/active_record/railties/databases.rake +144 -150
  166. data/lib/active_record/railties/job_runtime.rb +23 -0
  167. data/lib/active_record/readonly_attributes.rb +32 -5
  168. data/lib/active_record/reflection.rb +189 -45
  169. data/lib/active_record/relation/batches/batch_enumerator.rb +5 -3
  170. data/lib/active_record/relation/batches.rb +190 -61
  171. data/lib/active_record/relation/calculations.rb +232 -81
  172. data/lib/active_record/relation/delegation.rb +23 -9
  173. data/lib/active_record/relation/finder_methods.rb +77 -16
  174. data/lib/active_record/relation/merger.rb +2 -0
  175. data/lib/active_record/relation/predicate_builder/association_query_value.rb +31 -3
  176. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +10 -7
  177. data/lib/active_record/relation/predicate_builder/relation_handler.rb +5 -1
  178. data/lib/active_record/relation/predicate_builder.rb +26 -14
  179. data/lib/active_record/relation/query_attribute.rb +25 -1
  180. data/lib/active_record/relation/query_methods.rb +408 -76
  181. data/lib/active_record/relation/spawn_methods.rb +18 -1
  182. data/lib/active_record/relation.rb +103 -37
  183. data/lib/active_record/result.rb +25 -9
  184. data/lib/active_record/runtime_registry.rb +24 -1
  185. data/lib/active_record/sanitization.rb +51 -11
  186. data/lib/active_record/schema.rb +2 -3
  187. data/lib/active_record/schema_dumper.rb +50 -7
  188. data/lib/active_record/schema_migration.rb +68 -33
  189. data/lib/active_record/scoping/default.rb +15 -5
  190. data/lib/active_record/scoping/named.rb +2 -2
  191. data/lib/active_record/scoping.rb +2 -1
  192. data/lib/active_record/secure_password.rb +60 -0
  193. data/lib/active_record/secure_token.rb +21 -3
  194. data/lib/active_record/signed_id.rb +7 -5
  195. data/lib/active_record/store.rb +9 -9
  196. data/lib/active_record/suppressor.rb +3 -1
  197. data/lib/active_record/table_metadata.rb +16 -3
  198. data/lib/active_record/tasks/database_tasks.rb +152 -108
  199. data/lib/active_record/tasks/mysql_database_tasks.rb +15 -6
  200. data/lib/active_record/tasks/postgresql_database_tasks.rb +16 -13
  201. data/lib/active_record/tasks/sqlite_database_tasks.rb +15 -7
  202. data/lib/active_record/test_fixtures.rb +114 -96
  203. data/lib/active_record/timestamp.rb +30 -16
  204. data/lib/active_record/token_for.rb +113 -0
  205. data/lib/active_record/touch_later.rb +11 -6
  206. data/lib/active_record/transactions.rb +39 -13
  207. data/lib/active_record/type/adapter_specific_registry.rb +1 -8
  208. data/lib/active_record/type/internal/timezone.rb +7 -2
  209. data/lib/active_record/type/serialized.rb +8 -4
  210. data/lib/active_record/type/time.rb +4 -0
  211. data/lib/active_record/validations/absence.rb +1 -1
  212. data/lib/active_record/validations/numericality.rb +5 -4
  213. data/lib/active_record/validations/presence.rb +5 -28
  214. data/lib/active_record/validations/uniqueness.rb +47 -2
  215. data/lib/active_record/validations.rb +8 -4
  216. data/lib/active_record/version.rb +1 -1
  217. data/lib/active_record.rb +130 -17
  218. data/lib/arel/errors.rb +10 -0
  219. data/lib/arel/factory_methods.rb +4 -0
  220. data/lib/arel/filter_predications.rb +1 -1
  221. data/lib/arel/nodes/and.rb +4 -0
  222. data/lib/arel/nodes/binary.rb +6 -1
  223. data/lib/arel/nodes/bound_sql_literal.rb +61 -0
  224. data/lib/arel/nodes/cte.rb +36 -0
  225. data/lib/arel/nodes/filter.rb +1 -1
  226. data/lib/arel/nodes/fragments.rb +35 -0
  227. data/lib/arel/nodes/homogeneous_in.rb +1 -9
  228. data/lib/arel/nodes/leading_join.rb +8 -0
  229. data/lib/arel/nodes/node.rb +111 -2
  230. data/lib/arel/nodes/sql_literal.rb +6 -0
  231. data/lib/arel/nodes/table_alias.rb +4 -0
  232. data/lib/arel/nodes.rb +4 -0
  233. data/lib/arel/predications.rb +2 -0
  234. data/lib/arel/table.rb +9 -5
  235. data/lib/arel/tree_manager.rb +5 -1
  236. data/lib/arel/visitors/mysql.rb +8 -1
  237. data/lib/arel/visitors/to_sql.rb +83 -18
  238. data/lib/arel/visitors/visitor.rb +2 -2
  239. data/lib/arel.rb +16 -2
  240. data/lib/rails/generators/active_record/application_record/USAGE +8 -0
  241. data/lib/rails/generators/active_record/migration.rb +3 -1
  242. data/lib/rails/generators/active_record/model/USAGE +113 -0
  243. data/lib/rails/generators/active_record/model/model_generator.rb +15 -6
  244. metadata +51 -15
  245. data/lib/active_record/connection_adapters/legacy_pool_manager.rb +0 -35
  246. 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,8 +284,10 @@ 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 abstract_class?
280
- superclass == Base ? nil : superclass.table_name
287
+ self.table_name = if self == Base
288
+ nil
289
+ elsif abstract_class?
290
+ superclass.table_name
281
291
  elsif superclass.abstract_class?
282
292
  superclass.table_name || compute_table_name
283
293
  else
@@ -315,11 +325,7 @@ module ActiveRecord
315
325
  # The list of columns names the model should ignore. Ignored columns won't have attribute
316
326
  # accessors defined, and won't be referenced in SQL queries.
317
327
  def ignored_columns
318
- if defined?(@ignored_columns)
319
- @ignored_columns
320
- else
321
- superclass.ignored_columns
322
- end
328
+ @ignored_columns || superclass.ignored_columns
323
329
  end
324
330
 
325
331
  # Sets the columns names the model should ignore. Ignored columns won't have attribute
@@ -340,7 +346,7 @@ module ActiveRecord
340
346
  # # name :string, limit: 255
341
347
  # # category :string, limit: 255
342
348
  #
343
- # self.ignored_columns = [:category]
349
+ # self.ignored_columns += [:category]
344
350
  # end
345
351
  #
346
352
  # The schema still contains "category", but now the model omits it, so any meta-driven code or
@@ -425,6 +431,16 @@ module ActiveRecord
425
431
  @columns ||= columns_hash.values.freeze
426
432
  end
427
433
 
434
+ def _returning_columns_for_insert # :nodoc:
435
+ @_returning_columns_for_insert ||= begin
436
+ auto_populated_columns = columns.filter_map do |c|
437
+ c.name if connection.return_value_after_insert?(c)
438
+ end
439
+
440
+ auto_populated_columns.empty? ? Array(primary_key) : auto_populated_columns
441
+ end
442
+ end
443
+
428
444
  def attribute_types # :nodoc:
429
445
  load_schema
430
446
  @attribute_types ||= Hash.new(Type.default_value)
@@ -457,7 +473,7 @@ module ActiveRecord
457
473
  end
458
474
 
459
475
  # Returns the column object for the named attribute.
460
- # Returns an +ActiveRecord::ConnectionAdapters::NullColumn+ if the
476
+ # Returns an ActiveRecord::ConnectionAdapters::NullColumn if the
461
477
  # named attribute does not exist.
462
478
  #
463
479
  # class Person < ActiveRecord::Base
@@ -515,7 +531,7 @@ module ActiveRecord
515
531
  # when just after creating a table you want to populate it with some default
516
532
  # values, e.g.:
517
533
  #
518
- # class CreateJobLevels < ActiveRecord::Migration[7.0]
534
+ # class CreateJobLevels < ActiveRecord::Migration[7.1]
519
535
  # def up
520
536
  # create_table :job_levels do |t|
521
537
  # t.integer :id
@@ -543,35 +559,61 @@ module ActiveRecord
543
559
  initialize_find_by_cache
544
560
  end
545
561
 
562
+ def load_schema # :nodoc:
563
+ return if schema_loaded?
564
+ @load_schema_monitor.synchronize do
565
+ return if @columns_hash
566
+
567
+ load_schema!
568
+
569
+ @schema_loaded = true
570
+ rescue
571
+ reload_schema_from_cache # If the schema loading failed half way through, we must reset the state.
572
+ raise
573
+ end
574
+ end
575
+
546
576
  protected
547
577
  def initialize_load_schema_monitor
548
578
  @load_schema_monitor = Monitor.new
549
579
  end
550
580
 
581
+ def reload_schema_from_cache(recursive = true)
582
+ @_returning_columns_for_insert = nil
583
+ @arel_table = nil
584
+ @column_names = nil
585
+ @symbol_column_to_string_name_hash = nil
586
+ @attribute_types = nil
587
+ @content_columns = nil
588
+ @default_attributes = nil
589
+ @column_defaults = nil
590
+ @attributes_builder = nil
591
+ @columns = nil
592
+ @columns_hash = nil
593
+ @schema_loaded = false
594
+ @attribute_names = nil
595
+ @yaml_encoder = nil
596
+ if recursive
597
+ subclasses.each do |descendant|
598
+ descendant.send(:reload_schema_from_cache)
599
+ end
600
+ end
601
+ end
602
+
551
603
  private
552
604
  def inherited(child_class)
553
605
  super
554
606
  child_class.initialize_load_schema_monitor
607
+ child_class.reload_schema_from_cache(false)
608
+ child_class.class_eval do
609
+ @ignored_columns = nil
610
+ end
555
611
  end
556
612
 
557
613
  def schema_loaded?
558
614
  defined?(@schema_loaded) && @schema_loaded
559
615
  end
560
616
 
561
- def load_schema
562
- return if schema_loaded?
563
- @load_schema_monitor.synchronize do
564
- return if defined?(@columns_hash) && @columns_hash
565
-
566
- load_schema!
567
-
568
- @schema_loaded = true
569
- rescue
570
- reload_schema_from_cache # If the schema loading failed half way through, we must reset the state.
571
- raise
572
- end
573
- end
574
-
575
617
  def load_schema!
576
618
  unless table_name
577
619
  raise ActiveRecord::TableNotSpecified, "#{self} has no table configured. Set one with #{self}.table_name="
@@ -589,26 +631,9 @@ module ActiveRecord
589
631
  default: column.default,
590
632
  user_provided_default: false
591
633
  )
634
+ alias_attribute :id_value, :id if name == "id"
592
635
  end
593
- end
594
-
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
636
+ _default_attributes # Precompute to cache DB-dependent attribute types
612
637
  end
613
638
 
614
639
  # 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,24 @@ 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 for
287
+ # nested attributes.
288
+ #
289
+ # Integration test params should reflect the structure of the form. For
290
+ # example:
291
+ #
292
+ # post members_path, params: {
293
+ # member: {
294
+ # name: 'joe',
295
+ # posts_attributes: {
296
+ # '0' => { title: 'Foo' },
297
+ # '1' => { title: 'Bar' }
298
+ # }
299
+ # }
300
+ # }
283
301
  module ClassMethods
284
302
  REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == "_destroy" || value.blank? } }
285
303
 
@@ -289,7 +307,7 @@ module ActiveRecord
289
307
  # [:allow_destroy]
290
308
  # If true, destroys any members from the attributes hash with a
291
309
  # <tt>_destroy</tt> key and a value that evaluates to +true+
292
- # (e.g. 1, '1', true, or 'true'). This option is off by default.
310
+ # (e.g. 1, '1', true, or 'true'). This option is false by default.
293
311
  # [:reject_if]
294
312
  # Allows you to specify a Proc or a Symbol pointing to a method
295
313
  # that checks whether a record should be built for a certain attribute
@@ -314,11 +332,11 @@ module ActiveRecord
314
332
  # nested attributes are going to be used when an associated record already
315
333
  # exists. In general, an existing record may either be updated with the
316
334
  # 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+
335
+ # containing those values. By default the +:update_only+ option is false
318
336
  # and the nested attributes are used to update the existing record only
319
337
  # if they include the record's <tt>:id</tt> value. Otherwise a new
320
338
  # record will be instantiated and used to replace the existing one.
321
- # However if the +:update_only+ option is +true+, the nested attributes
339
+ # However if the +:update_only+ option is true, the nested attributes
322
340
  # are used to update the record's attributes always, regardless of
323
341
  # whether the <tt>:id</tt> is present. The option is ignored for collection
324
342
  # associations.
@@ -375,11 +393,11 @@ module ActiveRecord
375
393
  end
376
394
  end
377
395
 
378
- # Returns ActiveRecord::AutosaveAssociation::marked_for_destruction? It's
396
+ # Returns ActiveRecord::AutosaveAssociation#marked_for_destruction? It's
379
397
  # used in conjunction with fields_for to build a form element for the
380
398
  # destruction of this association.
381
399
  #
382
- # See ActionView::Helpers::FormHelper::fields_for for more info.
400
+ # See ActionView::Helpers::FormHelper#fields_for for more info.
383
401
  def _destroy
384
402
  marked_for_destruction?
385
403
  end
@@ -501,12 +519,12 @@ module ActiveRecord
501
519
  unless reject_new_record?(association_name, attributes)
502
520
  association.reader.build(attributes.except(*UNASSIGNABLE_KEYS))
503
521
  end
504
- elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes["id"].to_s }
522
+ elsif existing_record = find_record_by_id(existing_records, attributes["id"])
505
523
  unless call_reject_if(association_name, attributes)
506
524
  # Make sure we are operating on the actual object which is in the association's
507
525
  # proxy_target array (either by finding it, or adding it if not found)
508
526
  # Take into account that the proxy_target may have changed due to callbacks
509
- target_record = association.target.detect { |record| record.id.to_s == attributes["id"].to_s }
527
+ target_record = find_record_by_id(association.target, attributes["id"])
510
528
  if target_record
511
529
  existing_record = target_record
512
530
  else
@@ -594,5 +612,16 @@ module ActiveRecord
594
612
  raise RecordNotFound.new("Couldn't find #{model} with ID=#{record_id} for #{self.class.name} with ID=#{id}",
595
613
  model, "id", record_id)
596
614
  end
615
+
616
+ def find_record_by_id(records, id)
617
+ return if records.empty?
618
+
619
+ if records.first.class.composite_primary_key?
620
+ id = Array(id).map(&:to_s)
621
+ records.find { |record| Array(record.id).map(&:to_s) == id }
622
+ else
623
+ records.find { |record| record.id.to_s == id.to_s }
624
+ end
625
+ end
597
626
  end
598
627
  end
@@ -0,0 +1,167 @@
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
+ # Be aware that if your app was created before Rails 7.1, and your app
53
+ # marshals instances of the targeted model (for example, when caching),
54
+ # then you should set ActiveRecord.marshalling_format_version to +7.1+ or
55
+ # higher via either <tt>config.load_defaults 7.1</tt> or
56
+ # <tt>config.active_record.marshalling_format_version = 7.1</tt>.
57
+ # Otherwise, +Marshal+ may attempt to serialize the normalization +Proc+
58
+ # and raise +TypeError+.
59
+ #
60
+ # ==== Options
61
+ #
62
+ # * +:with+ - Any callable object that accepts the attribute's value as
63
+ # its sole argument, and returns it normalized.
64
+ # * +:apply_to_nil+ - Whether to apply the normalization to +nil+ values.
65
+ # Defaults to +false+.
66
+ #
67
+ # ==== Examples
68
+ #
69
+ # class User < ActiveRecord::Base
70
+ # normalizes :email, with: -> email { email.strip.downcase }
71
+ # normalizes :phone, with: -> phone { phone.delete("^0-9").delete_prefix("1") }
72
+ # end
73
+ #
74
+ # user = User.create(email: " CRUISE-CONTROL@EXAMPLE.COM\n")
75
+ # user.email # => "cruise-control@example.com"
76
+ #
77
+ # user = User.find_by(email: "\tCRUISE-CONTROL@EXAMPLE.COM ")
78
+ # user.email # => "cruise-control@example.com"
79
+ # user.email_before_type_cast # => "cruise-control@example.com"
80
+ #
81
+ # User.where(email: "\tCRUISE-CONTROL@EXAMPLE.COM ").count # => 1
82
+ # User.where(["email = ?", "\tCRUISE-CONTROL@EXAMPLE.COM "]).count # => 0
83
+ #
84
+ # User.exists?(email: "\tCRUISE-CONTROL@EXAMPLE.COM ") # => true
85
+ # User.exists?(["email = ?", "\tCRUISE-CONTROL@EXAMPLE.COM "]) # => false
86
+ #
87
+ # User.normalize_value_for(:phone, "+1 (555) 867-5309") # => "5558675309"
88
+ def normalizes(*names, with:, apply_to_nil: false)
89
+ names.each do |name|
90
+ attribute(name) do |cast_type|
91
+ NormalizedValueType.new(cast_type: cast_type, normalizer: with, normalize_nil: apply_to_nil)
92
+ end
93
+ end
94
+
95
+ self.normalized_attributes += names.map(&:to_sym)
96
+ end
97
+
98
+ # Normalizes a given +value+ using normalizations declared for +name+.
99
+ #
100
+ # ==== Examples
101
+ #
102
+ # class User < ActiveRecord::Base
103
+ # normalizes :email, with: -> email { email.strip.downcase }
104
+ # end
105
+ #
106
+ # User.normalize_value_for(:email, " CRUISE-CONTROL@EXAMPLE.COM\n")
107
+ # # => "cruise-control@example.com"
108
+ def normalize_value_for(name, value)
109
+ type_for_attribute(name).cast(value)
110
+ end
111
+ end
112
+
113
+ private
114
+ def normalize_changed_in_place_attributes
115
+ self.class.normalized_attributes.each do |name|
116
+ normalize_attribute(name) if attribute_changed_in_place?(name)
117
+ end
118
+ end
119
+
120
+ class NormalizedValueType < DelegateClass(ActiveModel::Type::Value) # :nodoc:
121
+ include ActiveModel::Type::SerializeCastValue
122
+
123
+ attr_reader :cast_type, :normalizer, :normalize_nil
124
+ alias :normalize_nil? :normalize_nil
125
+
126
+ def initialize(cast_type:, normalizer:, normalize_nil:)
127
+ @cast_type = cast_type
128
+ @normalizer = normalizer
129
+ @normalize_nil = normalize_nil
130
+ super(cast_type)
131
+ end
132
+
133
+ def cast(value)
134
+ normalize(super(value))
135
+ end
136
+
137
+ def serialize(value)
138
+ serialize_cast_value(cast(value))
139
+ end
140
+
141
+ def serialize_cast_value(value)
142
+ ActiveModel::Type::SerializeCastValue.serialize(cast_type, value)
143
+ end
144
+
145
+ def ==(other)
146
+ self.class == other.class &&
147
+ normalize_nil? == other.normalize_nil? &&
148
+ normalizer == other.normalizer &&
149
+ cast_type == other.cast_type
150
+ end
151
+ alias eql? ==
152
+
153
+ def hash
154
+ [self.class, cast_type, normalizer, normalize_nil?].hash
155
+ end
156
+
157
+ def inspect
158
+ Kernel.instance_method(:inspect).bind_call(self)
159
+ end
160
+
161
+ private
162
+ def normalize(value)
163
+ normalizer.call(value) unless value.nil? && !normalize_nil?
164
+ end
165
+ end
166
+ end
167
+ end