activerecord 7.0.0 → 7.1.0

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 (249) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1607 -1040
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +17 -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 +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 -12
  15. data/lib/active_record/associations/collection_proxy.rb +22 -12
  16. data/lib/active_record/associations/foreign_association.rb +10 -3
  17. data/lib/active_record/associations/has_many_association.rb +27 -17
  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 +20 -14
  21. data/lib/active_record/associations/preloader/association.rb +27 -6
  22. data/lib/active_record/associations/preloader/through_association.rb +1 -1
  23. data/lib/active_record/associations/preloader.rb +13 -10
  24. data/lib/active_record/associations/singular_association.rb +1 -1
  25. data/lib/active_record/associations/through_association.rb +22 -11
  26. data/lib/active_record/associations.rb +345 -219
  27. data/lib/active_record/attribute_assignment.rb +0 -2
  28. data/lib/active_record/attribute_methods/before_type_cast.rb +17 -0
  29. data/lib/active_record/attribute_methods/dirty.rb +40 -26
  30. data/lib/active_record/attribute_methods/primary_key.rb +76 -24
  31. data/lib/active_record/attribute_methods/query.rb +28 -16
  32. data/lib/active_record/attribute_methods/read.rb +18 -5
  33. data/lib/active_record/attribute_methods/serialization.rb +172 -69
  34. data/lib/active_record/attribute_methods/write.rb +3 -3
  35. data/lib/active_record/attribute_methods.rb +110 -28
  36. data/lib/active_record/attributes.rb +3 -3
  37. data/lib/active_record/autosave_association.rb +56 -10
  38. data/lib/active_record/base.rb +10 -5
  39. data/lib/active_record/callbacks.rb +16 -32
  40. data/lib/active_record/coders/column_serializer.rb +61 -0
  41. data/lib/active_record/coders/json.rb +1 -1
  42. data/lib/active_record/coders/yaml_column.rb +70 -34
  43. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +164 -89
  44. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +2 -0
  45. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +3 -1
  46. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +63 -43
  47. data/lib/active_record/connection_adapters/abstract/database_limits.rb +5 -0
  48. data/lib/active_record/connection_adapters/abstract/database_statements.rb +128 -32
  49. data/lib/active_record/connection_adapters/abstract/query_cache.rb +60 -22
  50. data/lib/active_record/connection_adapters/abstract/quoting.rb +52 -8
  51. data/lib/active_record/connection_adapters/abstract/savepoints.rb +4 -3
  52. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +18 -4
  53. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +163 -29
  54. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +14 -1
  55. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +302 -129
  56. data/lib/active_record/connection_adapters/abstract/transaction.rb +287 -58
  57. data/lib/active_record/connection_adapters/abstract_adapter.rb +504 -106
  58. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +217 -104
  59. data/lib/active_record/connection_adapters/column.rb +9 -0
  60. data/lib/active_record/connection_adapters/mysql/column.rb +1 -0
  61. data/lib/active_record/connection_adapters/mysql/database_statements.rb +23 -144
  62. data/lib/active_record/connection_adapters/mysql/quoting.rb +29 -12
  63. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +9 -0
  64. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +10 -1
  65. data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +8 -2
  66. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +38 -14
  67. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +148 -0
  68. data/lib/active_record/connection_adapters/mysql2_adapter.rb +98 -53
  69. data/lib/active_record/connection_adapters/pool_config.rb +14 -5
  70. data/lib/active_record/connection_adapters/pool_manager.rb +19 -9
  71. data/lib/active_record/connection_adapters/postgresql/column.rb +3 -2
  72. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +72 -45
  73. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +1 -1
  74. data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +2 -2
  75. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +11 -2
  76. data/lib/active_record/connection_adapters/postgresql/oid/timestamp_with_time_zone.rb +3 -1
  77. data/lib/active_record/connection_adapters/postgresql/quoting.rb +41 -8
  78. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +6 -10
  79. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +76 -6
  80. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +131 -2
  81. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +53 -0
  82. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +358 -57
  83. data/lib/active_record/connection_adapters/postgresql/utils.rb +9 -10
  84. data/lib/active_record/connection_adapters/postgresql_adapter.rb +343 -181
  85. data/lib/active_record/connection_adapters/schema_cache.rb +287 -59
  86. data/lib/active_record/connection_adapters/sqlite3/column.rb +49 -0
  87. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +45 -39
  88. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +22 -5
  89. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +7 -0
  90. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +41 -22
  91. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +242 -81
  92. data/lib/active_record/connection_adapters/statement_pool.rb +7 -0
  93. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +98 -0
  94. data/lib/active_record/connection_adapters/trilogy_adapter.rb +254 -0
  95. data/lib/active_record/connection_adapters.rb +3 -1
  96. data/lib/active_record/connection_handling.rb +73 -96
  97. data/lib/active_record/core.rb +136 -148
  98. data/lib/active_record/counter_cache.rb +46 -25
  99. data/lib/active_record/database_configurations/connection_url_resolver.rb +1 -0
  100. data/lib/active_record/database_configurations/database_config.rb +9 -3
  101. data/lib/active_record/database_configurations/hash_config.rb +22 -12
  102. data/lib/active_record/database_configurations/url_config.rb +17 -11
  103. data/lib/active_record/database_configurations.rb +87 -34
  104. data/lib/active_record/delegated_type.rb +9 -4
  105. data/lib/active_record/deprecator.rb +7 -0
  106. data/lib/active_record/destroy_association_async_job.rb +2 -0
  107. data/lib/active_record/disable_joins_association_relation.rb +1 -1
  108. data/lib/active_record/encryption/auto_filtered_parameters.rb +66 -0
  109. data/lib/active_record/encryption/cipher/aes256_gcm.rb +4 -1
  110. data/lib/active_record/encryption/config.rb +25 -1
  111. data/lib/active_record/encryption/configurable.rb +13 -14
  112. data/lib/active_record/encryption/context.rb +10 -3
  113. data/lib/active_record/encryption/contexts.rb +8 -4
  114. data/lib/active_record/encryption/derived_secret_key_provider.rb +9 -3
  115. data/lib/active_record/encryption/deterministic_key_provider.rb +1 -1
  116. data/lib/active_record/encryption/encryptable_record.rb +38 -22
  117. data/lib/active_record/encryption/encrypted_attribute_type.rb +19 -8
  118. data/lib/active_record/encryption/encryptor.rb +7 -7
  119. data/lib/active_record/encryption/envelope_encryption_key_provider.rb +3 -3
  120. data/lib/active_record/encryption/extended_deterministic_queries.rb +83 -71
  121. data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +3 -3
  122. data/lib/active_record/encryption/key_generator.rb +12 -1
  123. data/lib/active_record/encryption/message.rb +1 -1
  124. data/lib/active_record/encryption/message_serializer.rb +2 -0
  125. data/lib/active_record/encryption/properties.rb +4 -4
  126. data/lib/active_record/encryption/scheme.rb +20 -23
  127. data/lib/active_record/encryption.rb +1 -0
  128. data/lib/active_record/enum.rb +114 -27
  129. data/lib/active_record/errors.rb +108 -15
  130. data/lib/active_record/explain.rb +23 -3
  131. data/lib/active_record/explain_subscriber.rb +1 -1
  132. data/lib/active_record/fixture_set/model_metadata.rb +14 -4
  133. data/lib/active_record/fixture_set/render_context.rb +2 -0
  134. data/lib/active_record/fixture_set/table_row.rb +29 -8
  135. data/lib/active_record/fixtures.rb +121 -73
  136. data/lib/active_record/future_result.rb +30 -5
  137. data/lib/active_record/gem_version.rb +2 -2
  138. data/lib/active_record/inheritance.rb +30 -16
  139. data/lib/active_record/insert_all.rb +55 -8
  140. data/lib/active_record/integration.rb +10 -10
  141. data/lib/active_record/internal_metadata.rb +118 -30
  142. data/lib/active_record/locking/optimistic.rb +32 -18
  143. data/lib/active_record/locking/pessimistic.rb +8 -5
  144. data/lib/active_record/log_subscriber.rb +39 -17
  145. data/lib/active_record/marshalling.rb +56 -0
  146. data/lib/active_record/message_pack.rb +124 -0
  147. data/lib/active_record/middleware/database_selector/resolver.rb +4 -0
  148. data/lib/active_record/middleware/database_selector.rb +18 -13
  149. data/lib/active_record/middleware/shard_selector.rb +7 -5
  150. data/lib/active_record/migration/command_recorder.rb +104 -9
  151. data/lib/active_record/migration/compatibility.rb +158 -64
  152. data/lib/active_record/migration/default_strategy.rb +23 -0
  153. data/lib/active_record/migration/execution_strategy.rb +19 -0
  154. data/lib/active_record/migration.rb +271 -117
  155. data/lib/active_record/model_schema.rb +82 -50
  156. data/lib/active_record/nested_attributes.rb +23 -3
  157. data/lib/active_record/normalization.rb +159 -0
  158. data/lib/active_record/persistence.rb +200 -47
  159. data/lib/active_record/promise.rb +84 -0
  160. data/lib/active_record/query_cache.rb +3 -21
  161. data/lib/active_record/query_logs.rb +87 -51
  162. data/lib/active_record/query_logs_formatter.rb +41 -0
  163. data/lib/active_record/querying.rb +16 -3
  164. data/lib/active_record/railtie.rb +127 -61
  165. data/lib/active_record/railties/controller_runtime.rb +12 -8
  166. data/lib/active_record/railties/databases.rake +142 -143
  167. data/lib/active_record/railties/job_runtime.rb +23 -0
  168. data/lib/active_record/readonly_attributes.rb +32 -5
  169. data/lib/active_record/reflection.rb +177 -45
  170. data/lib/active_record/relation/batches/batch_enumerator.rb +5 -3
  171. data/lib/active_record/relation/batches.rb +190 -61
  172. data/lib/active_record/relation/calculations.rb +200 -83
  173. data/lib/active_record/relation/delegation.rb +23 -9
  174. data/lib/active_record/relation/finder_methods.rb +77 -16
  175. data/lib/active_record/relation/merger.rb +2 -0
  176. data/lib/active_record/relation/predicate_builder/association_query_value.rb +31 -3
  177. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +4 -6
  178. data/lib/active_record/relation/predicate_builder/relation_handler.rb +5 -1
  179. data/lib/active_record/relation/predicate_builder.rb +26 -14
  180. data/lib/active_record/relation/query_attribute.rb +25 -1
  181. data/lib/active_record/relation/query_methods.rb +429 -76
  182. data/lib/active_record/relation/spawn_methods.rb +18 -1
  183. data/lib/active_record/relation.rb +98 -41
  184. data/lib/active_record/result.rb +25 -9
  185. data/lib/active_record/runtime_registry.rb +10 -1
  186. data/lib/active_record/sanitization.rb +57 -16
  187. data/lib/active_record/schema.rb +36 -22
  188. data/lib/active_record/schema_dumper.rb +65 -23
  189. data/lib/active_record/schema_migration.rb +68 -33
  190. data/lib/active_record/scoping/default.rb +20 -12
  191. data/lib/active_record/scoping/named.rb +2 -2
  192. data/lib/active_record/scoping.rb +2 -1
  193. data/lib/active_record/secure_password.rb +60 -0
  194. data/lib/active_record/secure_token.rb +21 -3
  195. data/lib/active_record/serialization.rb +5 -0
  196. data/lib/active_record/signed_id.rb +9 -7
  197. data/lib/active_record/store.rb +16 -11
  198. data/lib/active_record/suppressor.rb +3 -1
  199. data/lib/active_record/table_metadata.rb +16 -3
  200. data/lib/active_record/tasks/database_tasks.rb +138 -107
  201. data/lib/active_record/tasks/mysql_database_tasks.rb +15 -6
  202. data/lib/active_record/tasks/postgresql_database_tasks.rb +17 -15
  203. data/lib/active_record/tasks/sqlite_database_tasks.rb +15 -7
  204. data/lib/active_record/test_fixtures.rb +123 -99
  205. data/lib/active_record/timestamp.rb +26 -14
  206. data/lib/active_record/token_for.rb +113 -0
  207. data/lib/active_record/touch_later.rb +11 -6
  208. data/lib/active_record/transactions.rb +39 -13
  209. data/lib/active_record/translation.rb +1 -1
  210. data/lib/active_record/type/adapter_specific_registry.rb +1 -8
  211. data/lib/active_record/type/internal/timezone.rb +7 -2
  212. data/lib/active_record/type/serialized.rb +8 -4
  213. data/lib/active_record/type/time.rb +4 -0
  214. data/lib/active_record/validations/absence.rb +1 -1
  215. data/lib/active_record/validations/associated.rb +3 -3
  216. data/lib/active_record/validations/numericality.rb +5 -4
  217. data/lib/active_record/validations/presence.rb +5 -28
  218. data/lib/active_record/validations/uniqueness.rb +50 -5
  219. data/lib/active_record/validations.rb +8 -4
  220. data/lib/active_record/version.rb +1 -1
  221. data/lib/active_record.rb +143 -16
  222. data/lib/arel/errors.rb +10 -0
  223. data/lib/arel/factory_methods.rb +4 -0
  224. data/lib/arel/filter_predications.rb +1 -1
  225. data/lib/arel/nodes/and.rb +4 -0
  226. data/lib/arel/nodes/binary.rb +6 -1
  227. data/lib/arel/nodes/bound_sql_literal.rb +61 -0
  228. data/lib/arel/nodes/cte.rb +36 -0
  229. data/lib/arel/nodes/filter.rb +1 -1
  230. data/lib/arel/nodes/fragments.rb +35 -0
  231. data/lib/arel/nodes/homogeneous_in.rb +0 -8
  232. data/lib/arel/nodes/leading_join.rb +8 -0
  233. data/lib/arel/nodes/node.rb +111 -2
  234. data/lib/arel/nodes/sql_literal.rb +6 -0
  235. data/lib/arel/nodes/table_alias.rb +4 -0
  236. data/lib/arel/nodes.rb +4 -0
  237. data/lib/arel/predications.rb +2 -0
  238. data/lib/arel/table.rb +9 -5
  239. data/lib/arel/visitors/mysql.rb +8 -1
  240. data/lib/arel/visitors/to_sql.rb +81 -17
  241. data/lib/arel/visitors/visitor.rb +2 -2
  242. data/lib/arel.rb +16 -2
  243. data/lib/rails/generators/active_record/application_record/USAGE +8 -0
  244. data/lib/rails/generators/active_record/migration.rb +3 -1
  245. data/lib/rails/generators/active_record/model/USAGE +113 -0
  246. data/lib/rails/generators/active_record/model/model_generator.rb +15 -6
  247. metadata +50 -15
  248. data/lib/active_record/connection_adapters/legacy_pool_manager.rb +0 -35
  249. data/lib/active_record/null_relation.rb +0 -63
@@ -6,7 +6,6 @@ require "active_record/relation/merger"
6
6
 
7
7
  module ActiveRecord
8
8
  module SpawnMethods
9
- # This is overridden by Associations::CollectionProxy
10
9
  def spawn # :nodoc:
11
10
  already_in_scope?(klass.scope_registry) ? klass.all : clone
12
11
  end
@@ -28,6 +27,9 @@ module ActiveRecord
28
27
  # # => Post.where(published: true).joins(:comments)
29
28
  #
30
29
  # This is mainly intended for sharing common conditions between multiple associations.
30
+ #
31
+ # For conditions that exist in both relations, those from <tt>other</tt> will take precedence.
32
+ # To find the intersection of two relations, use QueryMethods#and.
31
33
  def merge(other, *rest)
32
34
  if other.is_a?(Array)
33
35
  records & other
@@ -40,6 +42,21 @@ module ActiveRecord
40
42
 
41
43
  def merge!(other, *rest) # :nodoc:
42
44
  options = rest.extract_options!
45
+
46
+ if options.key?(:rewhere)
47
+ if options[:rewhere]
48
+ ActiveRecord.deprecator.warn(<<-MSG.squish)
49
+ Specifying `Relation#merge(rewhere: true)` is deprecated, as that has now been
50
+ the default since Rails 7.0. Setting the rewhere option will error in Rails 7.2
51
+ MSG
52
+ else
53
+ ActiveRecord.deprecator.warn(<<-MSG.squish)
54
+ `Relation#merge(rewhere: false)` is deprecated without replacement,
55
+ and will be removed in Rails 7.2
56
+ MSG
57
+ end
58
+ end
59
+
43
60
  if other.is_a?(Hash)
44
61
  Relation::HashMerger.new(self, other, options[:rewhere]).merge
45
62
  elsif other.is_a?(Relation)
@@ -5,13 +5,14 @@ module ActiveRecord
5
5
  class Relation
6
6
  MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group,
7
7
  :order, :joins, :left_outer_joins, :references,
8
- :extending, :unscope, :optimizer_hints, :annotate]
8
+ :extending, :unscope, :optimizer_hints, :annotate,
9
+ :with]
9
10
 
10
11
  SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering, :strict_loading,
11
12
  :reverse_order, :distinct, :create_with, :skip_query_cache]
12
13
 
13
14
  CLAUSE_METHODS = [:where, :having, :from]
14
- INVALID_METHODS_FOR_DELETE_ALL = [:distinct]
15
+ INVALID_METHODS_FOR_DELETE_ALL = [:distinct, :with]
15
16
 
16
17
  VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + CLAUSE_METHODS
17
18
 
@@ -33,6 +34,8 @@ module ActiveRecord
33
34
  @delegate_to_klass = false
34
35
  @future_result = nil
35
36
  @records = nil
37
+ @async = false
38
+ @none = false
36
39
  end
37
40
 
38
41
  def initialize_copy(other)
@@ -43,7 +46,7 @@ module ActiveRecord
43
46
  def bind_attribute(name, value) # :nodoc:
44
47
  if reflection = klass._reflect_on_association(name)
45
48
  name = reflection.foreign_key
46
- value = value.read_attribute(reflection.klass.primary_key) unless value.nil?
49
+ value = value.read_attribute(reflection.association_primary_key) unless value.nil?
47
50
  end
48
51
 
49
52
  attr = table[name]
@@ -159,21 +162,25 @@ module ActiveRecord
159
162
  # failed due to validation errors it won't be persisted, you get what
160
163
  # #create returns in such situation.
161
164
  #
162
- # Please note <b>this method is not atomic</b>, it runs first a SELECT, and if
163
- # there are no results an INSERT is attempted. If there are other threads
164
- # or processes there is a race condition between both calls and it could
165
- # be the case that you end up with two similar records.
165
+ # If creation failed because of a unique constraint, this method will
166
+ # assume it encountered a race condition and will try finding the record
167
+ # once more If somehow the second find still find no record because a
168
+ # concurrent DELETE happened, it will then raise an
169
+ # ActiveRecord::RecordNotFound exception.
166
170
  #
167
- # If this might be a problem for your application, please see #create_or_find_by.
171
+ # Please note <b>this method is not atomic</b>, it runs first a SELECT,
172
+ # and if there are no results an INSERT is attempted. So if the table
173
+ # doesn't have a relevant unique constraint it could be the case that
174
+ # you end up with two or more similar records.
168
175
  def find_or_create_by(attributes, &block)
169
- find_by(attributes) || create(attributes, &block)
176
+ find_by(attributes) || create_or_find_by(attributes, &block)
170
177
  end
171
178
 
172
179
  # Like #find_or_create_by, but calls
173
180
  # {create!}[rdoc-ref:Persistence::ClassMethods#create!] so an exception
174
181
  # is raised if the created record is invalid.
175
182
  def find_or_create_by!(attributes, &block)
176
- find_by(attributes) || create!(attributes, &block)
183
+ find_by(attributes) || create_or_find_by!(attributes, &block)
177
184
  end
178
185
 
179
186
  # Attempts to create a record with the given attributes in a table that has a unique database constraint
@@ -181,16 +188,15 @@ module ActiveRecord
181
188
  # unique constraints, the exception such an insertion would normally raise is caught,
182
189
  # and the existing record with those attributes is found using #find_by!.
183
190
  #
184
- # This is similar to #find_or_create_by, but avoids the problem of stale reads between the SELECT
185
- # and the INSERT, as that method needs to first query the table, then attempt to insert a row
186
- # if none is found.
191
+ # This is similar to #find_or_create_by, but tries to create the record first. As such it is
192
+ # better suited for cases where the record is most likely not to exist yet.
187
193
  #
188
194
  # There are several drawbacks to #create_or_find_by, though:
189
195
  #
190
196
  # * The underlying table must have the relevant columns defined with unique database constraints.
191
197
  # * A unique constraint violation may be triggered by only one, or at least less than all,
192
198
  # of the given attributes. This means that the subsequent #find_by! may fail to find a
193
- # matching record, which will then raise an <tt>ActiveRecord::RecordNotFound</tt> exception,
199
+ # matching record, which will then raise an ActiveRecord::RecordNotFound exception,
194
200
  # rather than a record with the given attributes.
195
201
  # * While we avoid the race condition between SELECT -> INSERT from #find_or_create_by,
196
202
  # we actually have another race condition between INSERT -> SELECT, which can be triggered
@@ -199,7 +205,7 @@ module ActiveRecord
199
205
  # * It relies on exception handling to handle control flow, which may be marginally slower.
200
206
  # * The primary key may auto-increment on each create, even if it fails. This can accelerate
201
207
  # the problem of running out of integers, if the underlying table is still stuck on a primary
202
- # key of type int (note: All Rails apps since 5.1+ have defaulted to bigint, which is not liable
208
+ # key of type int (note: All \Rails apps since 5.1+ have defaulted to bigint, which is not liable
203
209
  # to this problem).
204
210
  #
205
211
  # This method will return a record if all given attributes are covered by unique constraints
@@ -209,7 +215,11 @@ module ActiveRecord
209
215
  def create_or_find_by(attributes, &block)
210
216
  transaction(requires_new: true) { create(attributes, &block) }
211
217
  rescue ActiveRecord::RecordNotUnique
212
- find_by!(attributes)
218
+ if connection.transaction_open?
219
+ where(attributes).lock.find_by!(attributes)
220
+ else
221
+ find_by!(attributes)
222
+ end
213
223
  end
214
224
 
215
225
  # Like #create_or_find_by, but calls
@@ -218,7 +228,11 @@ module ActiveRecord
218
228
  def create_or_find_by!(attributes, &block)
219
229
  transaction(requires_new: true) { create!(attributes, &block) }
220
230
  rescue ActiveRecord::RecordNotUnique
221
- find_by!(attributes)
231
+ if connection.transaction_open?
232
+ where(attributes).lock.find_by!(attributes)
233
+ else
234
+ find_by!(attributes)
235
+ end
222
236
  end
223
237
 
224
238
  # Like #find_or_create_by, but calls {new}[rdoc-ref:Core#new]
@@ -236,8 +250,8 @@ module ActiveRecord
236
250
  #
237
251
  # Please see further details in the
238
252
  # {Active Record Query Interface guide}[https://guides.rubyonrails.org/active_record_querying.html#running-explain].
239
- def explain
240
- exec_explain(collecting_queries_for_explain { exec_queries })
253
+ def explain(*options)
254
+ exec_explain(collecting_queries_for_explain { exec_queries }, options)
241
255
  end
242
256
 
243
257
  # Converts relation objects to Array.
@@ -267,6 +281,8 @@ module ActiveRecord
267
281
 
268
282
  # Returns true if there are no records.
269
283
  def empty?
284
+ return true if @none
285
+
270
286
  if loaded?
271
287
  records.empty?
272
288
  else
@@ -275,26 +291,34 @@ module ActiveRecord
275
291
  end
276
292
 
277
293
  # Returns true if there are no records.
278
- def none?
279
- return super if block_given?
294
+ def none?(*args)
295
+ return true if @none
296
+
297
+ return super if args.present? || block_given?
280
298
  empty?
281
299
  end
282
300
 
283
301
  # Returns true if there are any records.
284
- def any?
285
- return super if block_given?
302
+ def any?(*args)
303
+ return false if @none
304
+
305
+ return super if args.present? || block_given?
286
306
  !empty?
287
307
  end
288
308
 
289
309
  # Returns true if there is exactly one record.
290
- def one?
291
- return super if block_given?
310
+ def one?(*args)
311
+ return false if @none
312
+
313
+ return super if args.present? || block_given?
292
314
  return records.one? if loaded?
293
315
  limited_count == 1
294
316
  end
295
317
 
296
318
  # Returns true if there is more than one record.
297
319
  def many?
320
+ return false if @none
321
+
298
322
  return super if block_given?
299
323
  return records.many? if loaded?
300
324
  limited_count > 1
@@ -307,7 +331,7 @@ module ActiveRecord
307
331
  # # => "products/query-1850ab3d302391b85b8693e941286659"
308
332
  #
309
333
  # If ActiveRecord::Base.collection_cache_versioning is turned off, as it was
310
- # in Rails 6.0 and earlier, the cache key will also include a version.
334
+ # in \Rails 6.0 and earlier, the cache key will also include a version.
311
335
  #
312
336
  # ActiveRecord::Base.collection_cache_versioning = false
313
337
  # Product.where("name like ?", "%Cosmic Encounter%").cache_key
@@ -388,7 +412,7 @@ module ActiveRecord
388
412
  end
389
413
 
390
414
  if timestamp
391
- "#{size}-#{timestamp.utc.to_formatted_s(cache_timestamp_format)}"
415
+ "#{size}-#{timestamp.utc.to_fs(cache_timestamp_format)}"
392
416
  else
393
417
  "#{size}"
394
418
  end
@@ -409,7 +433,7 @@ module ActiveRecord
409
433
  # Comment.where(post_id: 1).scoping do
410
434
  # Comment.first
411
435
  # end
412
- # # => SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1 ORDER BY "comments"."id" ASC LIMIT 1
436
+ # # SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1 ORDER BY "comments"."id" ASC LIMIT 1
413
437
  #
414
438
  # If <tt>all_queries: true</tt> is passed, scoping will apply to all queries
415
439
  # for the relation including +update+ and +delete+ on instances.
@@ -429,10 +453,10 @@ module ActiveRecord
429
453
  end
430
454
  end
431
455
 
432
- def _exec_scope(*args, &block) # :nodoc:
456
+ def _exec_scope(...) # :nodoc:
433
457
  @delegate_to_klass = true
434
458
  registry = klass.scope_registry
435
- _scoping(nil, registry) { instance_exec(*args, &block) || self }
459
+ _scoping(nil, registry) { instance_exec(...) || self }
436
460
  ensure
437
461
  @delegate_to_klass = false
438
462
  end
@@ -446,7 +470,8 @@ module ActiveRecord
446
470
  #
447
471
  # ==== Parameters
448
472
  #
449
- # * +updates+ - A string, array, or hash representing the SET part of an SQL statement.
473
+ # * +updates+ - A string, array, or hash representing the SET part of an SQL statement. Any strings provided will
474
+ # be type cast, unless you use +Arel.sql+. (Don't pass user-provided values to +Arel.sql+.)
450
475
  #
451
476
  # ==== Examples
452
477
  #
@@ -461,9 +486,14 @@ module ActiveRecord
461
486
  #
462
487
  # # Update all invoices and set the number column to its id value.
463
488
  # Invoice.update_all('number = id')
489
+ #
490
+ # # Update all books with 'Rails' in their title
491
+ # Book.where('title LIKE ?', '%Rails%').update_all(title: Arel.sql("title + ' - volume 1'"))
464
492
  def update_all(updates)
465
493
  raise ArgumentError, "Empty list of attributes to change" if updates.blank?
466
494
 
495
+ return 0 if @none
496
+
467
497
  if updates.is_a?(Hash)
468
498
  if klass.locking_enabled? &&
469
499
  !updates.key?(klass.locking_column) &&
@@ -599,6 +629,8 @@ module ActiveRecord
599
629
  # Post.distinct.delete_all
600
630
  # # => ActiveRecord::ActiveRecordError: delete_all doesn't support distinct
601
631
  def delete_all
632
+ return 0 if @none
633
+
602
634
  invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method|
603
635
  value = @values[method]
604
636
  method == :distinct ? value : value&.any?
@@ -646,6 +678,21 @@ module ActiveRecord
646
678
  # Schedule the query to be performed from a background thread pool.
647
679
  #
648
680
  # Post.where(published: true).load_async # => #<ActiveRecord::Relation>
681
+ #
682
+ # When the +Relation+ is iterated, if the background query wasn't executed yet,
683
+ # it will be performed by the foreground thread.
684
+ #
685
+ # Note that {config.active_record.async_query_executor}[https://guides.rubyonrails.org/configuring.html#config-active-record-async-query-executor] must be configured
686
+ # for queries to actually be executed concurrently. Otherwise it defaults to
687
+ # executing them in the foreground.
688
+ #
689
+ # +load_async+ will also fall back to executing in the foreground in the test environment when transactional
690
+ # fixtures are enabled.
691
+ #
692
+ # If the query was actually executed in the background, the Active Record logs will show
693
+ # it by prefixing the log line with <tt>ASYNC</tt>:
694
+ #
695
+ # ASYNC Post Load (0.0ms) (db time 2ms) SELECT "posts".* FROM "posts" LIMIT 100
649
696
  def load_async
650
697
  return load if !connection.async_enabled?
651
698
 
@@ -697,6 +744,7 @@ module ActiveRecord
697
744
  @to_sql = @arel = @loaded = @should_eager_load = nil
698
745
  @offsets = @take = nil
699
746
  @cache_keys = nil
747
+ @cache_versions = nil
700
748
  @records = nil
701
749
  self
702
750
  end
@@ -704,7 +752,7 @@ module ActiveRecord
704
752
  # Returns sql statement for the relation.
705
753
  #
706
754
  # User.where(name: 'Oscar').to_sql
707
- # # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar'
755
+ # # SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar'
708
756
  def to_sql
709
757
  @to_sql ||= if eager_loading?
710
758
  apply_join_dependency do |relation, join_dependency|
@@ -721,7 +769,7 @@ module ActiveRecord
721
769
  #
722
770
  # User.where(name: 'Oscar').where_values_hash
723
771
  # # => {name: "Oscar"}
724
- def where_values_hash(relation_table_name = klass.table_name)
772
+ def where_values_hash(relation_table_name = klass.table_name) # :nodoc:
725
773
  where_clause.to_h(relation_table_name)
726
774
  end
727
775
 
@@ -741,7 +789,7 @@ module ActiveRecord
741
789
  # Joins that are also marked for preloading. In which case we should just eager load them.
742
790
  # Note that this is a naive implementation because we could have strings and symbols which
743
791
  # represent the same association, but that aren't matched by this. Also, we could have
744
- # nested hashes which partially match, e.g. { a: :b } & { a: [:b, :c] }
792
+ # nested hashes which partially match, e.g. <tt>{ a: :b } & { a: [:b, :c] }</tt>
745
793
  def joined_includes_values
746
794
  includes_values & joins_values
747
795
  end
@@ -758,8 +806,13 @@ module ActiveRecord
758
806
  end
759
807
  end
760
808
 
761
- def pretty_print(q)
762
- q.pp(records)
809
+ def pretty_print(pp)
810
+ subject = loaded? ? records : annotate("loading for pp")
811
+ entries = subject.take([limit_value, 11].compact.min)
812
+
813
+ entries[10] = "..." if entries.size == 11
814
+
815
+ pp.pp(entries)
763
816
  end
764
817
 
765
818
  # Returns true if relation is blank.
@@ -821,10 +874,6 @@ module ActiveRecord
821
874
  @loaded = true
822
875
  end
823
876
 
824
- def null_relation? # :nodoc:
825
- is_a?(NullRelation)
826
- end
827
-
828
877
  private
829
878
  def already_in_scope?(registry)
830
879
  @delegate_to_klass && registry.current_scope(klass, true)
@@ -902,13 +951,21 @@ module ActiveRecord
902
951
  preload_associations(records) unless skip_preloading_value
903
952
 
904
953
  records.each(&:readonly!) if readonly_value
905
- records.each(&:strict_loading!) if strict_loading_value
954
+ records.each { |record| record.strict_loading!(strict_loading_value) } unless strict_loading_value.nil?
906
955
 
907
956
  records
908
957
  end
909
958
  end
910
959
 
911
960
  def exec_main_query(async: false)
961
+ if @none
962
+ if async
963
+ return Promise::Complete.new([])
964
+ else
965
+ return []
966
+ end
967
+ end
968
+
912
969
  skip_query_cache_if_necessary do
913
970
  if where_clause.contradiction?
914
971
  [].freeze
@@ -2,6 +2,8 @@
2
2
 
3
3
  module ActiveRecord
4
4
  ###
5
+ # = Active Record \Result
6
+ #
5
7
  # This class encapsulates a result returned from calling
6
8
  # {#exec_query}[rdoc-ref:ConnectionAdapters::DatabaseStatements#exec_query]
7
9
  # on any database connection adapter. For example:
@@ -36,8 +38,12 @@ module ActiveRecord
36
38
 
37
39
  attr_reader :columns, :rows, :column_types
38
40
 
39
- def self.empty # :nodoc:
40
- EMPTY
41
+ def self.empty(async: false) # :nodoc:
42
+ if async
43
+ EMPTY_ASYNC
44
+ else
45
+ EMPTY
46
+ end
41
47
  end
42
48
 
43
49
  def initialize(columns, rows, column_types = {})
@@ -47,9 +53,6 @@ module ActiveRecord
47
53
  @column_types = column_types
48
54
  end
49
55
 
50
- EMPTY = new([].freeze, [].freeze, {}.freeze)
51
- private_constant :EMPTY
52
-
53
56
  # Returns true if this result set includes the column named +name+
54
57
  def includes_column?(name)
55
58
  @columns.include? name
@@ -108,7 +111,7 @@ module ActiveRecord
108
111
  type = if type_overrides.is_a?(Array)
109
112
  type_overrides.first
110
113
  else
111
- column_type(columns.first, type_overrides)
114
+ column_type(columns.first, 0, type_overrides)
112
115
  end
113
116
 
114
117
  rows.map do |(value)|
@@ -118,7 +121,7 @@ module ActiveRecord
118
121
  types = if type_overrides.is_a?(Array)
119
122
  type_overrides
120
123
  else
121
- columns.map { |name| column_type(name, type_overrides) }
124
+ columns.map.with_index { |name, i| column_type(name, i, type_overrides) }
122
125
  end
123
126
 
124
127
  rows.map do |values|
@@ -134,10 +137,17 @@ module ActiveRecord
134
137
  @hash_rows = nil
135
138
  end
136
139
 
140
+ def freeze # :nodoc:
141
+ hash_rows.freeze
142
+ super
143
+ end
144
+
137
145
  private
138
- def column_type(name, type_overrides = {})
146
+ def column_type(name, index, type_overrides)
139
147
  type_overrides.fetch(name) do
140
- column_types.fetch(name, Type.default_value)
148
+ column_types.fetch(index) do
149
+ column_types.fetch(name, Type.default_value)
150
+ end
141
151
  end
142
152
  end
143
153
 
@@ -181,5 +191,11 @@ module ActiveRecord
181
191
  }
182
192
  end
183
193
  end
194
+
195
+ EMPTY = new([].freeze, [].freeze, {}.freeze).freeze
196
+ private_constant :EMPTY
197
+
198
+ EMPTY_ASYNC = FutureResult::Complete.new(EMPTY).freeze
199
+ private_constant :EMPTY_ASYNC
184
200
  end
185
201
  end
@@ -10,11 +10,20 @@ module ActiveRecord
10
10
  extend self
11
11
 
12
12
  def sql_runtime
13
- ActiveSupport::IsolatedExecutionState[:active_record_sql_runtime]
13
+ ActiveSupport::IsolatedExecutionState[:active_record_sql_runtime] ||= 0.0
14
14
  end
15
15
 
16
16
  def sql_runtime=(runtime)
17
17
  ActiveSupport::IsolatedExecutionState[:active_record_sql_runtime] = runtime
18
18
  end
19
+
20
+ def reset
21
+ rt, self.sql_runtime = sql_runtime, 0.0
22
+ rt
23
+ end
19
24
  end
20
25
  end
26
+
27
+ ActiveSupport::Notifications.monotonic_subscribe("sql.active_record") do |name, start, finish, id, payload|
28
+ ActiveRecord::RuntimeRegistry.sql_runtime += (finish - start) * 1_000.0
29
+ end
@@ -5,8 +5,8 @@ module ActiveRecord
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  module ClassMethods
8
- # Accepts an array or string of SQL conditions and sanitizes
9
- # them into a valid SQL fragment for a WHERE clause.
8
+ # Accepts an array of SQL conditions and sanitizes them into a valid
9
+ # SQL fragment for a WHERE clause.
10
10
  #
11
11
  # sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4])
12
12
  # # => "name='foo''bar' and group_id=4"
@@ -17,8 +17,19 @@ module ActiveRecord
17
17
  # sanitize_sql_for_conditions(["name='%s' and group_id='%s'", "foo'bar", 4])
18
18
  # # => "name='foo''bar' and group_id='4'"
19
19
  #
20
+ # This method will NOT sanitize a SQL string since it won't contain
21
+ # any conditions in it and will return the string as is.
22
+ #
20
23
  # sanitize_sql_for_conditions("name='foo''bar' and group_id='4'")
21
24
  # # => "name='foo''bar' and group_id='4'"
25
+ #
26
+ # Note that this sanitization method is not schema-aware, hence won't do any type casting
27
+ # and will directly use the database adapter's +quote+ method.
28
+ # For MySQL specifically this means that numeric parameters will be quoted as strings
29
+ # to prevent query manipulation attacks.
30
+ #
31
+ # sanitize_sql_for_conditions(["role = ?", 0])
32
+ # # => "role = '0'"
22
33
  def sanitize_sql_for_conditions(condition)
23
34
  return nil if condition.blank?
24
35
 
@@ -29,8 +40,8 @@ module ActiveRecord
29
40
  end
30
41
  alias :sanitize_sql :sanitize_sql_for_conditions
31
42
 
32
- # Accepts an array, hash, or string of SQL conditions and sanitizes
33
- # them into a valid SQL fragment for a SET clause.
43
+ # Accepts an array or hash of SQL conditions and sanitizes them into
44
+ # a valid SQL fragment for a SET clause.
34
45
  #
35
46
  # sanitize_sql_for_assignment(["name=? and group_id=?", nil, 4])
36
47
  # # => "name=NULL and group_id=4"
@@ -41,8 +52,19 @@ module ActiveRecord
41
52
  # Post.sanitize_sql_for_assignment({ name: nil, group_id: 4 })
42
53
  # # => "`posts`.`name` = NULL, `posts`.`group_id` = 4"
43
54
  #
55
+ # This method will NOT sanitize a SQL string since it won't contain
56
+ # any conditions in it and will return the string as is.
57
+ #
44
58
  # sanitize_sql_for_assignment("name=NULL and group_id='4'")
45
59
  # # => "name=NULL and group_id='4'"
60
+ #
61
+ # Note that this sanitization method is not schema-aware, hence won't do any type casting
62
+ # and will directly use the database adapter's +quote+ method.
63
+ # For MySQL specifically this means that numeric parameters will be quoted as strings
64
+ # to prevent query manipulation attacks.
65
+ #
66
+ # sanitize_sql_for_assignment(["role = ?", 0])
67
+ # # => "role = '0'"
46
68
  def sanitize_sql_for_assignment(assignments, default_table_name = table_name)
47
69
  case assignments
48
70
  when Array; sanitize_sql_array(assignments)
@@ -92,26 +114,32 @@ module ActiveRecord
92
114
  end
93
115
 
94
116
  # Sanitizes a +string+ so that it is safe to use within an SQL
95
- # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%".
117
+ # LIKE statement. This method uses +escape_character+ to escape all
118
+ # occurrences of itself, "_" and "%".
96
119
  #
97
- # sanitize_sql_like("100%")
98
- # # => "100\\%"
120
+ # sanitize_sql_like("100% true!")
121
+ # # => "100\\% true!"
99
122
  #
100
123
  # sanitize_sql_like("snake_cased_string")
101
124
  # # => "snake\\_cased\\_string"
102
125
  #
103
- # sanitize_sql_like("100%", "!")
104
- # # => "100!%"
126
+ # sanitize_sql_like("100% true!", "!")
127
+ # # => "100!% true!!"
105
128
  #
106
129
  # sanitize_sql_like("snake_cased_string", "!")
107
130
  # # => "snake!_cased!_string"
108
131
  def sanitize_sql_like(string, escape_character = "\\")
109
- pattern = Regexp.union(escape_character, "%", "_")
110
- string.gsub(pattern) { |x| [escape_character, x].join }
132
+ if string.include?(escape_character) && escape_character != "%" && escape_character != "_"
133
+ string = string.gsub(escape_character, '\0\0')
134
+ end
135
+
136
+ string.gsub(/(?=[%_])/, escape_character)
111
137
  end
112
138
 
113
139
  # Accepts an array of conditions. The array has each value
114
- # sanitized and interpolated into the SQL statement.
140
+ # sanitized and interpolated into the SQL statement. If using named bind
141
+ # variables in SQL statements where a colon is required verbatim use a
142
+ # backslash to escape.
115
143
  #
116
144
  # sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4])
117
145
  # # => "name='foo''bar' and group_id=4"
@@ -119,8 +147,19 @@ module ActiveRecord
119
147
  # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
120
148
  # # => "name='foo''bar' and group_id=4"
121
149
  #
150
+ # sanitize_sql_array(["TO_TIMESTAMP(:date, 'YYYY/MM/DD HH12\\:MI\\:SS')", date: "foo"])
151
+ # # => "TO_TIMESTAMP('foo', 'YYYY/MM/DD HH12:MI:SS')"
152
+ #
122
153
  # sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4])
123
154
  # # => "name='foo''bar' and group_id='4'"
155
+ #
156
+ # Note that this sanitization method is not schema-aware, hence won't do any type casting
157
+ # and will directly use the database adapter's +quote+ method.
158
+ # For MySQL specifically this means that numeric parameters will be quoted as strings
159
+ # to prevent query manipulation attacks.
160
+ #
161
+ # sanitize_sql_array(["role = ?", 0])
162
+ # # => "role = '0'"
124
163
  def sanitize_sql_array(ary)
125
164
  statement, *values = ary
126
165
  if values.first.is_a?(Hash) && /:\w+/.match?(statement)
@@ -172,9 +211,11 @@ module ActiveRecord
172
211
  end
173
212
 
174
213
  def replace_named_bind_variables(statement, bind_vars)
175
- statement.gsub(/(:?):([a-zA-Z]\w*)/) do |match|
214
+ statement.gsub(/([:\\]?):([a-zA-Z]\w*)/) do |match|
176
215
  if $1 == ":" # skip postgresql casts
177
216
  match # return the whole match
217
+ elsif $1 == "\\" # escaped literal colon
218
+ match[1..-1] # return match with escaping backlash char removed
178
219
  elsif bind_vars.include?(match = $2.to_sym)
179
220
  replace_bind_variable(bind_vars[match])
180
221
  else
@@ -187,13 +228,13 @@ module ActiveRecord
187
228
  if value.respond_to?(:map) && !value.acts_like?(:string)
188
229
  values = value.map { |v| v.respond_to?(:id_for_database) ? v.id_for_database : v }
189
230
  if values.empty?
190
- c.quote_bound_value(nil)
231
+ c.quote(c.cast_bound_value(nil))
191
232
  else
192
- values.map! { |v| c.quote_bound_value(v) }.join(",")
233
+ values.map! { |v| c.quote(c.cast_bound_value(v)) }.join(",")
193
234
  end
194
235
  else
195
236
  value = value.id_for_database if value.respond_to?(:id_for_database)
196
- c.quote_bound_value(value)
237
+ c.quote(c.cast_bound_value(value))
197
238
  end
198
239
  end
199
240