activerecord 7.0.0 → 7.1.0

Sign up to get free protection for your applications and to get access to all the features.
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