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
@@ -4,6 +4,38 @@ require "active_support/core_ext/module/attribute_accessors"
4
4
 
5
5
  module ActiveRecord
6
6
  module AttributeMethods
7
+ # = Active Record Attribute Methods \Dirty
8
+ #
9
+ # Provides a way to track changes in your Active Record models. It adds all
10
+ # methods from ActiveModel::Dirty and adds database specific methods.
11
+ #
12
+ # A newly created +Person+ object is unchanged:
13
+ #
14
+ # class Person < ActiveRecord::Base
15
+ # end
16
+ #
17
+ # person = Person.create(name: "Alisson")
18
+ # person.changed? # => false
19
+ #
20
+ # Change the name:
21
+ #
22
+ # person.name = 'Alice'
23
+ # person.name_in_database # => "Allison"
24
+ # person.will_save_change_to_name? # => true
25
+ # person.name_change_to_be_saved # => ["Allison", "Alice"]
26
+ # person.changes_to_save # => {"name"=>["Allison", "Alice"]}
27
+ #
28
+ # Save the changes:
29
+ #
30
+ # person.save
31
+ # person.name_in_database # => "Alice"
32
+ # person.saved_change_to_name? # => true
33
+ # person.saved_change_to_name # => ["Allison", "Alice"]
34
+ # person.name_before_last_change # => "Allison"
35
+ #
36
+ # Similar to ActiveModel::Dirty, methods can be invoked as
37
+ # +saved_change_to_name?+ or by passing an argument to the generic method
38
+ # <tt>saved_change_to_attribute?("name")</tt>.
7
39
  module Dirty
8
40
  extend ActiveSupport::Concern
9
41
 
@@ -27,32 +59,6 @@ module ActiveRecord
27
59
  attribute_method_suffix("_change_to_be_saved", "_in_database", parameters: false)
28
60
  end
29
61
 
30
- module ClassMethods
31
- def partial_writes
32
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
33
- ActiveRecord::Base.partial_writes is deprecated and will be removed in Rails 7.1.
34
- Use `partial_updates` and `partial_inserts` instead.
35
- MSG
36
- partial_updates && partial_inserts
37
- end
38
-
39
- def partial_writes?
40
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
41
- `ActiveRecord::Base.partial_writes?` is deprecated and will be removed in Rails 7.1.
42
- Use `partial_updates?` and `partial_inserts?` instead.
43
- MSG
44
- partial_updates? && partial_inserts?
45
- end
46
-
47
- def partial_writes=(value)
48
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
49
- `ActiveRecord::Base.partial_writes=` is deprecated and will be removed in Rails 7.1.
50
- Use `partial_updates=` and `partial_inserts=` instead.
51
- MSG
52
- self.partial_updates = self.partial_inserts = value
53
- end
54
- end
55
-
56
62
  # <tt>reload</tt> the record and clears changed attributes.
57
63
  def reload(*)
58
64
  super.tap do
@@ -183,6 +189,14 @@ module ActiveRecord
183
189
  end
184
190
 
185
191
  private
192
+ def init_internals
193
+ super
194
+ @mutations_before_last_save = nil
195
+ @mutations_from_database = nil
196
+ @_touch_attr_names = nil
197
+ @_skip_dirty_tracking = nil
198
+ end
199
+
186
200
  def _touch_row(attribute_names, time)
187
201
  @_touch_attr_names = Set.new(attribute_names)
188
202
 
@@ -4,6 +4,7 @@ require "set"
4
4
 
5
5
  module ActiveRecord
6
6
  module AttributeMethods
7
+ # = Active Record Attribute Methods Primary Key
7
8
  module PrimaryKey
8
9
  extend ActiveSupport::Concern
9
10
 
@@ -11,41 +12,80 @@ module ActiveRecord
11
12
  # available.
12
13
  def to_key
13
14
  key = id
14
- [key] if key
15
+ Array(key) if key
15
16
  end
16
17
 
17
- # Returns the primary key column's value.
18
+ # Returns the primary key column's value. If the primary key is composite,
19
+ # returns an array of the primary key column values.
18
20
  def id
19
- _read_attribute(@primary_key)
21
+ return _read_attribute(@primary_key) unless @primary_key.is_a?(Array)
22
+
23
+ @primary_key.map { |pk| _read_attribute(pk) }
24
+ end
25
+
26
+ def primary_key_values_present? # :nodoc:
27
+ return id.all? if self.class.composite_primary_key?
28
+
29
+ !!id
20
30
  end
21
31
 
22
- # Sets the primary key column's value.
32
+ # Sets the primary key column's value. If the primary key is composite,
33
+ # raises TypeError when the set value not enumerable.
23
34
  def id=(value)
24
- _write_attribute(@primary_key, value)
35
+ if self.class.composite_primary_key?
36
+ raise TypeError, "Expected value matching #{self.class.primary_key.inspect}, got #{value.inspect}." unless value.is_a?(Enumerable)
37
+ @primary_key.zip(value) { |attr, value| _write_attribute(attr, value) }
38
+ else
39
+ _write_attribute(@primary_key, value)
40
+ end
25
41
  end
26
42
 
27
- # Queries the primary key column's value.
43
+ # Queries the primary key column's value. If the primary key is composite,
44
+ # all primary key column values must be queryable.
28
45
  def id?
29
- query_attribute(@primary_key)
46
+ if self.class.composite_primary_key?
47
+ @primary_key.all? { |col| _query_attribute(col) }
48
+ else
49
+ _query_attribute(@primary_key)
50
+ end
30
51
  end
31
52
 
32
- # Returns the primary key column's value before type cast.
53
+ # Returns the primary key column's value before type cast. If the primary key is composite,
54
+ # returns an array of primary key column values before type cast.
33
55
  def id_before_type_cast
34
- attribute_before_type_cast(@primary_key)
56
+ if self.class.composite_primary_key?
57
+ @primary_key.map { |col| attribute_before_type_cast(col) }
58
+ else
59
+ attribute_before_type_cast(@primary_key)
60
+ end
35
61
  end
36
62
 
37
- # Returns the primary key column's previous value.
63
+ # Returns the primary key column's previous value. If the primary key is composite,
64
+ # returns an array of primary key column previous values.
38
65
  def id_was
39
- attribute_was(@primary_key)
66
+ if self.class.composite_primary_key?
67
+ @primary_key.map { |col| attribute_was(col) }
68
+ else
69
+ attribute_was(@primary_key)
70
+ end
40
71
  end
41
72
 
42
- # Returns the primary key column's value from the database.
73
+ # Returns the primary key column's value from the database. If the primary key is composite,
74
+ # returns an array of primary key column values from database.
43
75
  def id_in_database
44
- attribute_in_database(@primary_key)
76
+ if self.class.composite_primary_key?
77
+ @primary_key.map { |col| attribute_in_database(col) }
78
+ else
79
+ attribute_in_database(@primary_key)
80
+ end
45
81
  end
46
82
 
47
83
  def id_for_database # :nodoc:
48
- @attributes[@primary_key].value_for_database
84
+ if self.class.composite_primary_key?
85
+ @primary_key.map { |col| @attributes[col].value_for_database }
86
+ else
87
+ @attributes[@primary_key].value_for_database
88
+ end
49
89
  end
50
90
 
51
91
  private
@@ -55,6 +95,7 @@ module ActiveRecord
55
95
 
56
96
  module ClassMethods
57
97
  ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was id_in_database id_for_database).to_set
98
+ PRIMARY_KEY_NOT_SET = BasicObject.new
58
99
 
59
100
  def instance_method_already_implemented?(method_name)
60
101
  super || primary_key && ID_ATTRIBUTE_METHODS.include?(method_name)
@@ -68,10 +109,16 @@ module ActiveRecord
68
109
  # Overwriting will negate any effect of the +primary_key_prefix_type+
69
110
  # setting, though.
70
111
  def primary_key
71
- @primary_key = reset_primary_key unless defined? @primary_key
112
+ if PRIMARY_KEY_NOT_SET.equal?(@primary_key)
113
+ @primary_key = reset_primary_key
114
+ end
72
115
  @primary_key
73
116
  end
74
117
 
118
+ def composite_primary_key? # :nodoc:
119
+ primary_key.is_a?(Array)
120
+ end
121
+
75
122
  # Returns a quoted version of the primary key name, used to construct
76
123
  # SQL statements.
77
124
  def quoted_primary_key
@@ -93,8 +140,7 @@ module ActiveRecord
93
140
  base_name.foreign_key
94
141
  else
95
142
  if ActiveRecord::Base != self && table_exists?
96
- pk = connection.schema_cache.primary_keys(table_name)
97
- suppress_composite_primary_key(pk)
143
+ connection.schema_cache.primary_keys(table_name)
98
144
  else
99
145
  "id"
100
146
  end
@@ -117,20 +163,26 @@ module ActiveRecord
117
163
  #
118
164
  # Project.primary_key # => "foo_id"
119
165
  def primary_key=(value)
120
- @primary_key = value && -value.to_s
166
+ @primary_key = derive_primary_key(value)
121
167
  @quoted_primary_key = nil
122
168
  @attributes_builder = nil
123
169
  end
124
170
 
125
171
  private
126
- def suppress_composite_primary_key(pk)
127
- return pk unless pk.is_a?(Array)
172
+ def derive_primary_key(value)
173
+ return unless value
174
+
175
+ return -value.to_s unless value.is_a?(Array)
128
176
 
129
- warn <<~WARNING
130
- WARNING: Active Record does not support composite primary key.
177
+ value.map { |v| -v.to_s }.freeze
178
+ end
131
179
 
132
- #{table_name} has composite primary key. Composite primary key is ignored.
133
- WARNING
180
+ def inherited(base)
181
+ super
182
+ base.class_eval do
183
+ @primary_key = PRIMARY_KEY_NOT_SET
184
+ @quoted_primary_key = nil
185
+ end
134
186
  end
135
187
  end
136
188
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module AttributeMethods
5
+ # = Active Record Attribute Methods \Query
5
6
  module Query
6
7
  extend ActiveSupport::Concern
7
8
 
@@ -12,27 +13,38 @@ module ActiveRecord
12
13
  def query_attribute(attr_name)
13
14
  value = self.public_send(attr_name)
14
15
 
15
- case value
16
- when true then true
17
- when false, nil then false
18
- else
19
- if !type_for_attribute(attr_name) { false }
20
- if Numeric === value || !value.match?(/[^0-9]/)
21
- !value.to_i.zero?
16
+ query_cast_attribute(attr_name, value)
17
+ end
18
+
19
+ def _query_attribute(attr_name) # :nodoc:
20
+ value = self._read_attribute(attr_name.to_s)
21
+
22
+ query_cast_attribute(attr_name, value)
23
+ end
24
+
25
+ alias :attribute? :query_attribute
26
+ private :attribute?
27
+
28
+ private
29
+ def query_cast_attribute(attr_name, value)
30
+ case value
31
+ when true then true
32
+ when false, nil then false
33
+ else
34
+ if !type_for_attribute(attr_name) { false }
35
+ if Numeric === value || !value.match?(/[^0-9]/)
36
+ !value.to_i.zero?
37
+ else
38
+ return false if ActiveModel::Type::Boolean::FALSE_VALUES.include?(value)
39
+ !value.blank?
40
+ end
41
+ elsif value.respond_to?(:zero?)
42
+ !value.zero?
22
43
  else
23
- return false if ActiveModel::Type::Boolean::FALSE_VALUES.include?(value)
24
44
  !value.blank?
25
45
  end
26
- elsif value.respond_to?(:zero?)
27
- !value.zero?
28
- else
29
- !value.blank?
30
46
  end
31
47
  end
32
- end
33
-
34
- alias :attribute? :query_attribute
35
- private :attribute?
36
48
  end
37
49
  end
38
50
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module AttributeMethods
5
+ # = Active Record Attribute Methods \Read
5
6
  module Read
6
7
  extend ActiveSupport::Concern
7
8
 
@@ -21,15 +22,27 @@ module ActiveRecord
21
22
  end
22
23
  end
23
24
 
24
- # Returns the value of the attribute identified by <tt>attr_name</tt> after
25
- # it has been typecast (for example, "2004-12-12" in a date column is cast
26
- # to a date object, like <tt>Date.new(2004, 12, 12)</tt>).
25
+ # Returns the value of the attribute identified by +attr_name+ after it
26
+ # has been type cast. For example, a date attribute will cast "2004-12-12"
27
+ # to <tt>Date.new(2004, 12, 12)</tt>. (For information about specific type
28
+ # casting behavior, see the types under ActiveModel::Type.)
27
29
  def read_attribute(attr_name, &block)
28
30
  name = attr_name.to_s
29
31
  name = self.class.attribute_aliases[name] || name
30
32
 
31
- name = @primary_key if name == "id" && @primary_key
32
- @attributes.fetch_value(name, &block)
33
+ return @attributes.fetch_value(name, &block) unless name == "id" && @primary_key
34
+
35
+ if self.class.composite_primary_key?
36
+ @attributes.fetch_value("id", &block)
37
+ else
38
+ if @primary_key != "id"
39
+ ActiveRecord.deprecator.warn(<<-MSG.squish)
40
+ Using read_attribute(:id) to read the primary key value is deprecated.
41
+ Use #id instead.
42
+ MSG
43
+ end
44
+ @attributes.fetch_value(@primary_key, &block)
45
+ end
33
46
  end
34
47
 
35
48
  # This method exists to avoid the expensive primary_key check internally, without
@@ -2,6 +2,7 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module AttributeMethods
5
+ # = Active Record Attribute Methods \Serialization
5
6
  module Serialization
6
7
  extend ActiveSupport::Concern
7
8
 
@@ -15,6 +16,10 @@ module ActiveRecord
15
16
  end
16
17
  end
17
18
 
19
+ included do
20
+ class_attribute :default_column_serializer, instance_accessor: false, default: Coders::YAMLColumn
21
+ end
22
+
18
23
  module ClassMethods
19
24
  # If you have an attribute that needs to be saved to the database as a
20
25
  # serialized object, and retrieved by deserializing into the same object,
@@ -36,21 +41,19 @@ module ActiveRecord
36
41
  # ==== Parameters
37
42
  #
38
43
  # * +attr_name+ - The name of the attribute to serialize.
39
- # * +class_name_or_coder+ - Optional. May be one of the following:
40
- # * <em>default</em> - The attribute value will be serialized as YAML.
41
- # The attribute value must respond to +to_yaml+.
42
- # * +Array+ - The attribute value will be serialized as YAML, but an
43
- # empty +Array+ will be serialized as +NULL+. The attribute value
44
- # must be an +Array+.
45
- # * +Hash+ - The attribute value will be serialized as YAML, but an
46
- # empty +Hash+ will be serialized as +NULL+. The attribute value
47
- # must be a +Hash+.
48
- # * +JSON+ - The attribute value will be serialized as JSON. The
49
- # attribute value must respond to +to_json+.
50
- # * <em>custom coder</em> - The attribute value will be serialized
44
+ # * +coder+ The serializer implementation to use, e.g. +JSON+.
45
+ # * The attribute value will be serialized
51
46
  # using the coder's <tt>dump(value)</tt> method, and will be
52
47
  # deserialized using the coder's <tt>load(string)</tt> method. The
53
48
  # +dump+ method may return +nil+ to serialize the value as +NULL+.
49
+ # * +type+ - Optional. What the type of the serialized object should be.
50
+ # * Attempting to serialize another type will raise an
51
+ # ActiveRecord::SerializationTypeMismatch error.
52
+ # * If the column is +NULL+ or starting from a new record, the default value
53
+ # will set to +type.new+
54
+ # * +yaml+ - Optional. Yaml specific options. The allowed config is:
55
+ # * +:permitted_classes+ - +Array+ with the permitted classes.
56
+ # * +:unsafe_load+ - Unsafely load YAML blobs, allow YAML to load any class.
54
57
  #
55
58
  # ==== Options
56
59
  #
@@ -58,24 +61,101 @@ module ActiveRecord
58
61
  # this option is not passed, the previous default value (if any) will
59
62
  # be used. Otherwise, the default will be +nil+.
60
63
  #
64
+ # ==== Choosing a serializer
65
+ #
66
+ # While any serialization format can be used, it is recommended to carefully
67
+ # evaluate the properties of a serializer before using it, as migrating to
68
+ # another format later on can be difficult.
69
+ #
70
+ # ===== Avoid accepting arbitrary types
71
+ #
72
+ # When serializing data in a column, it is heavily recommended to make sure
73
+ # only expected types will be serialized. For instance some serializer like
74
+ # +Marshal+ or +YAML+ are capable of serializing almost any Ruby object.
75
+ #
76
+ # This can lead to unexpected types being serialized, and it is important
77
+ # that type serialization remains backward and forward compatible as long
78
+ # as some database records still contain these serialized types.
79
+ #
80
+ # class Address
81
+ # def initialize(line, city, country)
82
+ # @line, @city, @country = line, city, country
83
+ # end
84
+ # end
85
+ #
86
+ # In the above example, if any of the +Address+ attributes is renamed,
87
+ # instances that were persisted before the change will be loaded with the
88
+ # old attributes. This problem is even worse when the serialized type comes
89
+ # from a dependency which doesn't expect to be serialized this way and may
90
+ # change its internal representation without notice.
91
+ #
92
+ # As such, it is heavily recommended to instead convert these objects into
93
+ # primitives of the serialization format, for example:
94
+ #
95
+ # class Address
96
+ # attr_reader :line, :city, :country
97
+ #
98
+ # def self.load(payload)
99
+ # data = YAML.safe_load(payload)
100
+ # new(data["line"], data["city"], data["country"])
101
+ # end
102
+ #
103
+ # def self.dump(address)
104
+ # YAML.safe_dump(
105
+ # "line" => address.line,
106
+ # "city" => address.city,
107
+ # "country" => address.country,
108
+ # )
109
+ # end
110
+ #
111
+ # def initialize(line, city, country)
112
+ # @line, @city, @country = line, city, country
113
+ # end
114
+ # end
115
+ #
116
+ # class User < ActiveRecord::Base
117
+ # serialize :address, coder: Address
118
+ # end
119
+ #
120
+ # This pattern allows to be more deliberate about what is serialized, and
121
+ # to evolve the format in a backward compatible way.
122
+ #
123
+ # ===== Ensure serialization stability
124
+ #
125
+ # Some serialization methods may accept some types they don't support by
126
+ # silently casting them to other types. This can cause bugs when the
127
+ # data is deserialized.
128
+ #
129
+ # For instance the +JSON+ serializer provided in the standard library will
130
+ # silently cast unsupported types to +String+:
131
+ #
132
+ # >> JSON.parse(JSON.dump(Struct.new(:foo)))
133
+ # => "#<Class:0x000000013090b4c0>"
134
+ #
61
135
  # ==== Examples
62
136
  #
63
137
  # ===== Serialize the +preferences+ attribute using YAML
64
138
  #
65
139
  # class User < ActiveRecord::Base
66
- # serialize :preferences
140
+ # serialize :preferences, coder: YAML
67
141
  # end
68
142
  #
69
143
  # ===== Serialize the +preferences+ attribute using JSON
70
144
  #
71
145
  # class User < ActiveRecord::Base
72
- # serialize :preferences, JSON
146
+ # serialize :preferences, coder: JSON
73
147
  # end
74
148
  #
75
149
  # ===== Serialize the +preferences+ +Hash+ using YAML
76
150
  #
77
151
  # class User < ActiveRecord::Base
78
- # serialize :preferences, Hash
152
+ # serialize :preferences, type: Hash, coder: YAML
153
+ # end
154
+ #
155
+ # ===== Serializes +preferences+ to YAML, permitting select classes
156
+ #
157
+ # class User < ActiveRecord::Base
158
+ # serialize :preferences, coder: YAML, yaml: { permitted_classes: [Symbol, Time] }
79
159
  # end
80
160
  #
81
161
  # ===== Serialize the +preferences+ attribute using a custom coder
@@ -97,35 +177,74 @@ module ActiveRecord
97
177
  # end
98
178
  #
99
179
  # class User < ActiveRecord::Base
100
- # serialize :preferences, Rot13JSON
180
+ # serialize :preferences, coder: Rot13JSON
101
181
  # end
102
182
  #
103
- def serialize(attr_name, class_name_or_coder = Object, **options)
104
- # When ::JSON is used, force it to go through the Active Support JSON encoder
105
- # to ensure special objects (e.g. Active Record models) are dumped correctly
106
- # using the #as_json hook.
107
- coder = if class_name_or_coder == ::JSON
108
- Coders::JSON
109
- elsif [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) }
110
- class_name_or_coder
111
- else
112
- Coders::YAMLColumn.new(attr_name, class_name_or_coder)
183
+ def serialize(attr_name, class_name_or_coder = nil, coder: nil, type: Object, yaml: {}, **options)
184
+ unless class_name_or_coder.nil?
185
+ if class_name_or_coder == ::JSON || [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) }
186
+ ActiveRecord.deprecator.warn(<<~MSG)
187
+ Passing the coder as positional argument is deprecated and will be removed in Rails 7.2.
188
+
189
+ Please pass the coder as a keyword argument:
190
+
191
+ serialize #{attr_name.inspect}, coder: #{class_name_or_coder}
192
+ MSG
193
+ coder = class_name_or_coder
194
+ else
195
+ ActiveRecord.deprecator.warn(<<~MSG)
196
+ Passing the class as positional argument is deprecated and will be removed in Rails 7.2.
197
+
198
+ Please pass the class as a keyword argument:
199
+
200
+ serialize #{attr_name.inspect}, type: #{class_name_or_coder.name}
201
+ MSG
202
+ type = class_name_or_coder
203
+ end
204
+ end
205
+
206
+ coder ||= default_column_serializer
207
+ unless coder
208
+ raise ArgumentError, <<~MSG.squish
209
+ missing keyword: :coder
210
+
211
+ If no default coder is configured, a coder must be provided to `serialize`.
212
+ MSG
113
213
  end
114
214
 
215
+ column_serializer = build_column_serializer(attr_name, coder, type, yaml)
216
+
115
217
  attribute(attr_name, **options) do |cast_type|
116
- if type_incompatible_with_serialize?(cast_type, class_name_or_coder)
218
+ if type_incompatible_with_serialize?(cast_type, coder, type)
117
219
  raise ColumnNotSerializableError.new(attr_name, cast_type)
118
220
  end
119
221
 
120
222
  cast_type = cast_type.subtype if Type::Serialized === cast_type
121
- Type::Serialized.new(cast_type, coder)
223
+ Type::Serialized.new(cast_type, column_serializer)
122
224
  end
123
225
  end
124
226
 
125
227
  private
126
- def type_incompatible_with_serialize?(type, class_name)
127
- type.is_a?(ActiveRecord::Type::Json) && class_name == ::JSON ||
128
- type.respond_to?(:type_cast_array, true) && class_name == ::Array
228
+ def build_column_serializer(attr_name, coder, type, yaml = nil)
229
+ # When ::JSON is used, force it to go through the Active Support JSON encoder
230
+ # to ensure special objects (e.g. Active Record models) are dumped correctly
231
+ # using the #as_json hook.
232
+ coder = Coders::JSON if coder == ::JSON
233
+
234
+ if coder == ::YAML || coder == Coders::YAMLColumn
235
+ Coders::YAMLColumn.new(attr_name, type, **(yaml || {}))
236
+ elsif coder.respond_to?(:new) && !coder.respond_to?(:load)
237
+ coder.new(attr_name, type)
238
+ elsif type && type != Object
239
+ Coders::ColumnSerializer.new(attr_name, coder, type)
240
+ else
241
+ coder
242
+ end
243
+ end
244
+
245
+ def type_incompatible_with_serialize?(cast_type, coder, type)
246
+ cast_type.is_a?(ActiveRecord::Type::Json) && coder == ::JSON ||
247
+ cast_type.respond_to?(:type_cast_array, true) && type == ::Array
129
248
  end
130
249
  end
131
250
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module AttributeMethods
5
+ # = Active Record Attribute Methods \Write
5
6
  module Write
6
7
  extend ActiveSupport::Concern
7
8
 
@@ -25,9 +26,8 @@ module ActiveRecord
25
26
  end
26
27
  end
27
28
 
28
- # Updates the attribute identified by <tt>attr_name</tt> with the
29
- # specified +value+. Empty strings for Integer and Float columns are
30
- # turned into +nil+.
29
+ # Updates the attribute identified by +attr_name+ using the specified
30
+ # +value+. The attribute value will be type cast upon being read.
31
31
  def write_attribute(attr_name, value)
32
32
  name = attr_name.to_s
33
33
  name = self.class.attribute_aliases[name] || name