activerecord 7.0.8 → 7.1.3.4

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 (231) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1554 -1452
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +16 -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 +20 -4
  8. data/lib/active_record/associations/association_scope.rb +16 -9
  9. data/lib/active_record/associations/belongs_to_association.rb +14 -6
  10. data/lib/active_record/associations/builder/association.rb +3 -3
  11. data/lib/active_record/associations/builder/belongs_to.rb +21 -8
  12. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +1 -5
  13. data/lib/active_record/associations/builder/singular_association.rb +4 -0
  14. data/lib/active_record/associations/collection_association.rb +15 -9
  15. data/lib/active_record/associations/collection_proxy.rb +15 -10
  16. data/lib/active_record/associations/foreign_association.rb +10 -3
  17. data/lib/active_record/associations/has_many_association.rb +20 -13
  18. data/lib/active_record/associations/has_many_through_association.rb +10 -6
  19. data/lib/active_record/associations/has_one_association.rb +10 -3
  20. data/lib/active_record/associations/join_dependency.rb +10 -8
  21. data/lib/active_record/associations/preloader/association.rb +31 -7
  22. data/lib/active_record/associations/preloader.rb +13 -10
  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 +313 -217
  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 +52 -34
  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 +74 -51
  46. data/lib/active_record/connection_adapters/abstract/database_limits.rb +5 -0
  47. data/lib/active_record/connection_adapters/abstract/database_statements.rb +129 -31
  48. data/lib/active_record/connection_adapters/abstract/query_cache.rb +62 -23
  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 -124
  54. data/lib/active_record/connection_adapters/abstract/transaction.rb +287 -58
  55. data/lib/active_record/connection_adapters/abstract_adapter.rb +511 -91
  56. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +207 -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 +18 -13
  65. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +151 -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 +14 -3
  70. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +74 -40
  71. data/lib/active_record/connection_adapters/postgresql/oid/money.rb +3 -2
  72. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +11 -2
  73. data/lib/active_record/connection_adapters/postgresql/oid/timestamp_with_time_zone.rb +1 -1
  74. data/lib/active_record/connection_adapters/postgresql/quoting.rb +10 -6
  75. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +3 -9
  76. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +76 -6
  77. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +131 -2
  78. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +53 -0
  79. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +361 -60
  80. data/lib/active_record/connection_adapters/postgresql_adapter.rb +353 -192
  81. data/lib/active_record/connection_adapters/schema_cache.rb +287 -59
  82. data/lib/active_record/connection_adapters/sqlite3/column.rb +49 -0
  83. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +52 -39
  84. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +4 -3
  85. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +1 -0
  86. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +26 -7
  87. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +209 -79
  88. data/lib/active_record/connection_adapters/statement_pool.rb +7 -0
  89. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +99 -0
  90. data/lib/active_record/connection_adapters/trilogy_adapter.rb +262 -0
  91. data/lib/active_record/connection_adapters.rb +3 -1
  92. data/lib/active_record/connection_handling.rb +72 -95
  93. data/lib/active_record/core.rb +175 -153
  94. data/lib/active_record/counter_cache.rb +46 -25
  95. data/lib/active_record/database_configurations/database_config.rb +9 -3
  96. data/lib/active_record/database_configurations/hash_config.rb +22 -12
  97. data/lib/active_record/database_configurations/url_config.rb +17 -11
  98. data/lib/active_record/database_configurations.rb +86 -33
  99. data/lib/active_record/delegated_type.rb +9 -4
  100. data/lib/active_record/deprecator.rb +7 -0
  101. data/lib/active_record/destroy_association_async_job.rb +2 -0
  102. data/lib/active_record/encryption/auto_filtered_parameters.rb +66 -0
  103. data/lib/active_record/encryption/cipher/aes256_gcm.rb +4 -1
  104. data/lib/active_record/encryption/config.rb +25 -1
  105. data/lib/active_record/encryption/configurable.rb +12 -19
  106. data/lib/active_record/encryption/context.rb +10 -3
  107. data/lib/active_record/encryption/contexts.rb +5 -1
  108. data/lib/active_record/encryption/derived_secret_key_provider.rb +8 -2
  109. data/lib/active_record/encryption/encryptable_record.rb +42 -18
  110. data/lib/active_record/encryption/encrypted_attribute_type.rb +21 -6
  111. data/lib/active_record/encryption/extended_deterministic_queries.rb +66 -69
  112. data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +3 -3
  113. data/lib/active_record/encryption/key_generator.rb +12 -1
  114. data/lib/active_record/encryption/message_serializer.rb +2 -0
  115. data/lib/active_record/encryption/properties.rb +3 -3
  116. data/lib/active_record/encryption/scheme.rb +19 -22
  117. data/lib/active_record/encryption.rb +1 -0
  118. data/lib/active_record/enum.rb +112 -28
  119. data/lib/active_record/errors.rb +112 -18
  120. data/lib/active_record/explain.rb +23 -3
  121. data/lib/active_record/fixture_set/model_metadata.rb +14 -4
  122. data/lib/active_record/fixture_set/render_context.rb +2 -0
  123. data/lib/active_record/fixture_set/table_row.rb +29 -8
  124. data/lib/active_record/fixtures.rb +135 -71
  125. data/lib/active_record/future_result.rb +31 -5
  126. data/lib/active_record/gem_version.rb +4 -4
  127. data/lib/active_record/inheritance.rb +30 -16
  128. data/lib/active_record/insert_all.rb +57 -10
  129. data/lib/active_record/integration.rb +8 -8
  130. data/lib/active_record/internal_metadata.rb +120 -30
  131. data/lib/active_record/locking/pessimistic.rb +5 -2
  132. data/lib/active_record/log_subscriber.rb +29 -12
  133. data/lib/active_record/marshalling.rb +56 -0
  134. data/lib/active_record/message_pack.rb +124 -0
  135. data/lib/active_record/middleware/database_selector/resolver.rb +4 -0
  136. data/lib/active_record/middleware/database_selector.rb +6 -8
  137. data/lib/active_record/middleware/shard_selector.rb +3 -1
  138. data/lib/active_record/migration/command_recorder.rb +104 -5
  139. data/lib/active_record/migration/compatibility.rb +139 -5
  140. data/lib/active_record/migration/default_strategy.rb +23 -0
  141. data/lib/active_record/migration/execution_strategy.rb +19 -0
  142. data/lib/active_record/migration/pending_migration_connection.rb +21 -0
  143. data/lib/active_record/migration.rb +219 -111
  144. data/lib/active_record/model_schema.rb +64 -44
  145. data/lib/active_record/nested_attributes.rb +24 -6
  146. data/lib/active_record/normalization.rb +167 -0
  147. data/lib/active_record/persistence.rb +188 -37
  148. data/lib/active_record/promise.rb +84 -0
  149. data/lib/active_record/query_cache.rb +3 -21
  150. data/lib/active_record/query_logs.rb +77 -52
  151. data/lib/active_record/query_logs_formatter.rb +41 -0
  152. data/lib/active_record/querying.rb +15 -2
  153. data/lib/active_record/railtie.rb +109 -47
  154. data/lib/active_record/railties/controller_runtime.rb +12 -6
  155. data/lib/active_record/railties/databases.rake +142 -148
  156. data/lib/active_record/railties/job_runtime.rb +23 -0
  157. data/lib/active_record/readonly_attributes.rb +32 -5
  158. data/lib/active_record/reflection.rb +174 -44
  159. data/lib/active_record/relation/batches/batch_enumerator.rb +5 -3
  160. data/lib/active_record/relation/batches.rb +190 -61
  161. data/lib/active_record/relation/calculations.rb +187 -63
  162. data/lib/active_record/relation/delegation.rb +23 -9
  163. data/lib/active_record/relation/finder_methods.rb +77 -16
  164. data/lib/active_record/relation/merger.rb +2 -0
  165. data/lib/active_record/relation/predicate_builder/association_query_value.rb +11 -2
  166. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +4 -6
  167. data/lib/active_record/relation/predicate_builder/relation_handler.rb +5 -1
  168. data/lib/active_record/relation/predicate_builder.rb +26 -14
  169. data/lib/active_record/relation/query_attribute.rb +2 -1
  170. data/lib/active_record/relation/query_methods.rb +352 -63
  171. data/lib/active_record/relation/spawn_methods.rb +18 -1
  172. data/lib/active_record/relation.rb +91 -35
  173. data/lib/active_record/result.rb +19 -5
  174. data/lib/active_record/runtime_registry.rb +24 -1
  175. data/lib/active_record/sanitization.rb +51 -11
  176. data/lib/active_record/schema.rb +2 -3
  177. data/lib/active_record/schema_dumper.rb +46 -7
  178. data/lib/active_record/schema_migration.rb +68 -33
  179. data/lib/active_record/scoping/default.rb +15 -5
  180. data/lib/active_record/scoping/named.rb +2 -2
  181. data/lib/active_record/scoping.rb +2 -1
  182. data/lib/active_record/secure_password.rb +60 -0
  183. data/lib/active_record/secure_token.rb +21 -3
  184. data/lib/active_record/signed_id.rb +7 -5
  185. data/lib/active_record/store.rb +8 -8
  186. data/lib/active_record/suppressor.rb +3 -1
  187. data/lib/active_record/table_metadata.rb +10 -1
  188. data/lib/active_record/tasks/database_tasks.rb +127 -105
  189. data/lib/active_record/tasks/mysql_database_tasks.rb +15 -6
  190. data/lib/active_record/tasks/postgresql_database_tasks.rb +16 -13
  191. data/lib/active_record/tasks/sqlite_database_tasks.rb +15 -7
  192. data/lib/active_record/test_fixtures.rb +113 -96
  193. data/lib/active_record/timestamp.rb +27 -15
  194. data/lib/active_record/token_for.rb +113 -0
  195. data/lib/active_record/touch_later.rb +11 -6
  196. data/lib/active_record/transactions.rb +36 -10
  197. data/lib/active_record/type/adapter_specific_registry.rb +1 -8
  198. data/lib/active_record/type/internal/timezone.rb +7 -2
  199. data/lib/active_record/type/time.rb +4 -0
  200. data/lib/active_record/validations/absence.rb +1 -1
  201. data/lib/active_record/validations/numericality.rb +5 -4
  202. data/lib/active_record/validations/presence.rb +5 -28
  203. data/lib/active_record/validations/uniqueness.rb +47 -2
  204. data/lib/active_record/validations.rb +8 -4
  205. data/lib/active_record/version.rb +1 -1
  206. data/lib/active_record.rb +121 -16
  207. data/lib/arel/errors.rb +10 -0
  208. data/lib/arel/factory_methods.rb +4 -0
  209. data/lib/arel/nodes/binary.rb +6 -1
  210. data/lib/arel/nodes/bound_sql_literal.rb +61 -0
  211. data/lib/arel/nodes/cte.rb +36 -0
  212. data/lib/arel/nodes/fragments.rb +35 -0
  213. data/lib/arel/nodes/homogeneous_in.rb +1 -9
  214. data/lib/arel/nodes/leading_join.rb +8 -0
  215. data/lib/arel/nodes/node.rb +111 -2
  216. data/lib/arel/nodes/sql_literal.rb +6 -0
  217. data/lib/arel/nodes/table_alias.rb +4 -0
  218. data/lib/arel/nodes.rb +4 -0
  219. data/lib/arel/predications.rb +2 -0
  220. data/lib/arel/table.rb +9 -5
  221. data/lib/arel/visitors/mysql.rb +8 -1
  222. data/lib/arel/visitors/to_sql.rb +81 -17
  223. data/lib/arel/visitors/visitor.rb +2 -2
  224. data/lib/arel.rb +16 -2
  225. data/lib/rails/generators/active_record/application_record/USAGE +8 -0
  226. data/lib/rails/generators/active_record/migration.rb +3 -1
  227. data/lib/rails/generators/active_record/model/USAGE +113 -0
  228. data/lib/rails/generators/active_record/model/model_generator.rb +15 -6
  229. metadata +48 -12
  230. data/lib/active_record/connection_adapters/legacy_pool_manager.rb +0 -35
  231. 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 does not find a record
168
+ # because a 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,49 @@ 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
+ #
295
+ # When a pattern argument is given, this method checks whether elements in
296
+ # the Enumerable match the pattern via the case-equality operator (<tt>===</tt>).
297
+ #
298
+ # posts.none?(Comment) # => true or false
299
+ def none?(*args)
300
+ return true if @none
301
+
302
+ return super if args.present? || block_given?
280
303
  empty?
281
304
  end
282
305
 
283
306
  # Returns true if there are any records.
284
- def any?
285
- return super if block_given?
307
+ #
308
+ # When a pattern argument is given, this method checks whether elements in
309
+ # the Enumerable match the pattern via the case-equality operator (<tt>===</tt>).
310
+ #
311
+ # posts.any?(Post) # => true or false
312
+ def any?(*args)
313
+ return false if @none
314
+
315
+ return super if args.present? || block_given?
286
316
  !empty?
287
317
  end
288
318
 
289
319
  # Returns true if there is exactly one record.
290
- def one?
291
- return super if block_given?
320
+ #
321
+ # When a pattern argument is given, this method checks whether elements in
322
+ # the Enumerable match the pattern via the case-equality operator (<tt>===</tt>).
323
+ #
324
+ # posts.one?(Post) # => true or false
325
+ def one?(*args)
326
+ return false if @none
327
+
328
+ return super if args.present? || block_given?
292
329
  return records.one? if loaded?
293
330
  limited_count == 1
294
331
  end
295
332
 
296
333
  # Returns true if there is more than one record.
297
334
  def many?
335
+ return false if @none
336
+
298
337
  return super if block_given?
299
338
  return records.many? if loaded?
300
339
  limited_count > 1
@@ -307,7 +346,7 @@ module ActiveRecord
307
346
  # # => "products/query-1850ab3d302391b85b8693e941286659"
308
347
  #
309
348
  # 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.
349
+ # in \Rails 6.0 and earlier, the cache key will also include a version.
311
350
  #
312
351
  # ActiveRecord::Base.collection_cache_versioning = false
313
352
  # Product.where("name like ?", "%Cosmic Encounter%").cache_key
@@ -409,7 +448,7 @@ module ActiveRecord
409
448
  # Comment.where(post_id: 1).scoping do
410
449
  # Comment.first
411
450
  # end
412
- # # => SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1 ORDER BY "comments"."id" ASC LIMIT 1
451
+ # # SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1 ORDER BY "comments"."id" ASC LIMIT 1
413
452
  #
414
453
  # If <tt>all_queries: true</tt> is passed, scoping will apply to all queries
415
454
  # for the relation including +update+ and +delete+ on instances.
@@ -446,7 +485,8 @@ module ActiveRecord
446
485
  #
447
486
  # ==== Parameters
448
487
  #
449
- # * +updates+ - A string, array, or hash representing the SET part of an SQL statement.
488
+ # * +updates+ - A string, array, or hash representing the SET part of an SQL statement. Any strings provided will
489
+ # be type cast, unless you use +Arel.sql+. (Don't pass user-provided values to +Arel.sql+.)
450
490
  #
451
491
  # ==== Examples
452
492
  #
@@ -461,9 +501,14 @@ module ActiveRecord
461
501
  #
462
502
  # # Update all invoices and set the number column to its id value.
463
503
  # Invoice.update_all('number = id')
504
+ #
505
+ # # Update all books with 'Rails' in their title
506
+ # Book.where('title LIKE ?', '%Rails%').update_all(title: Arel.sql("title + ' - volume 1'"))
464
507
  def update_all(updates)
465
508
  raise ArgumentError, "Empty list of attributes to change" if updates.blank?
466
509
 
510
+ return 0 if @none
511
+
467
512
  if updates.is_a?(Hash)
468
513
  if klass.locking_enabled? &&
469
514
  !updates.key?(klass.locking_column) &&
@@ -599,6 +644,8 @@ module ActiveRecord
599
644
  # Post.distinct.delete_all
600
645
  # # => ActiveRecord::ActiveRecordError: delete_all doesn't support distinct
601
646
  def delete_all
647
+ return 0 if @none
648
+
602
649
  invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method|
603
650
  value = @values[method]
604
651
  method == :distinct ? value : value&.any?
@@ -720,7 +767,7 @@ module ActiveRecord
720
767
  # Returns sql statement for the relation.
721
768
  #
722
769
  # User.where(name: 'Oscar').to_sql
723
- # # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar'
770
+ # # SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar'
724
771
  def to_sql
725
772
  @to_sql ||= if eager_loading?
726
773
  apply_join_dependency do |relation, join_dependency|
@@ -774,8 +821,13 @@ module ActiveRecord
774
821
  end
775
822
  end
776
823
 
777
- def pretty_print(q)
778
- q.pp(records)
824
+ def pretty_print(pp)
825
+ subject = loaded? ? records : annotate("loading for pp")
826
+ entries = subject.take([limit_value, 11].compact.min)
827
+
828
+ entries[10] = "..." if entries.size == 11
829
+
830
+ pp.pp(entries)
779
831
  end
780
832
 
781
833
  # Returns true if relation is blank.
@@ -837,10 +889,6 @@ module ActiveRecord
837
889
  @loaded = true
838
890
  end
839
891
 
840
- def null_relation? # :nodoc:
841
- is_a?(NullRelation)
842
- end
843
-
844
892
  private
845
893
  def already_in_scope?(registry)
846
894
  @delegate_to_klass && registry.current_scope(klass, true)
@@ -925,6 +973,14 @@ module ActiveRecord
925
973
  end
926
974
 
927
975
  def exec_main_query(async: false)
976
+ if @none
977
+ if async
978
+ return FutureResult::Complete.new([])
979
+ else
980
+ return []
981
+ end
982
+ end
983
+
928
984
  skip_query_cache_if_necessary do
929
985
  if where_clause.contradiction?
930
986
  [].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
@@ -134,6 +137,11 @@ 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
146
  def column_type(name, index, type_overrides)
139
147
  type_overrides.fetch(name) do
@@ -183,5 +191,11 @@ module ActiveRecord
183
191
  }
184
192
  end
185
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
186
200
  end
187
201
  end
@@ -10,11 +10,34 @@ 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 async_sql_runtime
21
+ ActiveSupport::IsolatedExecutionState[:active_record_async_sql_runtime] ||= 0.0
22
+ end
23
+
24
+ def async_sql_runtime=(runtime)
25
+ ActiveSupport::IsolatedExecutionState[:active_record_async_sql_runtime] = runtime
26
+ end
27
+
28
+ def reset
29
+ rt, self.sql_runtime = sql_runtime, 0.0
30
+ self.async_sql_runtime = 0.0
31
+ rt
32
+ end
33
+ end
34
+ end
35
+
36
+ ActiveSupport::Notifications.monotonic_subscribe("sql.active_record") do |name, start, finish, id, payload|
37
+ runtime = (finish - start) * 1_000.0
38
+
39
+ if payload[:async]
40
+ ActiveRecord::RuntimeRegistry.async_sql_runtime += (runtime - payload[:lock_wait])
19
41
  end
42
+ ActiveRecord::RuntimeRegistry.sql_runtime += runtime
20
43
  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)
@@ -107,12 +129,17 @@ module ActiveRecord
107
129
  # sanitize_sql_like("snake_cased_string", "!")
108
130
  # # => "snake!_cased!_string"
109
131
  def sanitize_sql_like(string, escape_character = "\\")
110
- pattern = Regexp.union(escape_character, "%", "_")
111
- 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)
112
137
  end
113
138
 
114
139
  # Accepts an array of conditions. The array has each value
115
- # 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.
116
143
  #
117
144
  # sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4])
118
145
  # # => "name='foo''bar' and group_id=4"
@@ -120,8 +147,19 @@ module ActiveRecord
120
147
  # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
121
148
  # # => "name='foo''bar' and group_id=4"
122
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
+ #
123
153
  # sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4])
124
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'"
125
163
  def sanitize_sql_array(ary)
126
164
  statement, *values = ary
127
165
  if values.first.is_a?(Hash) && /:\w+/.match?(statement)
@@ -173,9 +211,11 @@ module ActiveRecord
173
211
  end
174
212
 
175
213
  def replace_named_bind_variables(statement, bind_vars)
176
- statement.gsub(/(:?):([a-zA-Z]\w*)/) do |match|
214
+ statement.gsub(/([:\\]?):([a-zA-Z]\w*)/) do |match|
177
215
  if $1 == ":" # skip postgresql casts
178
216
  match # return the whole match
217
+ elsif $1 == "\\" # escaped literal colon
218
+ match[1..-1] # return match with escaping backlash char removed
179
219
  elsif bind_vars.include?(match = $2.to_sym)
180
220
  replace_bind_variable(bind_vars[match])
181
221
  else
@@ -188,13 +228,13 @@ module ActiveRecord
188
228
  if value.respond_to?(:map) && !value.acts_like?(:string)
189
229
  values = value.map { |v| v.respond_to?(:id_for_database) ? v.id_for_database : v }
190
230
  if values.empty?
191
- c.quote_bound_value(nil)
231
+ c.quote(c.cast_bound_value(nil))
192
232
  else
193
- values.map! { |v| c.quote_bound_value(v) }.join(",")
233
+ values.map! { |v| c.quote(c.cast_bound_value(v)) }.join(",")
194
234
  end
195
235
  else
196
236
  value = value.id_for_database if value.respond_to?(:id_for_database)
197
- c.quote_bound_value(value)
237
+ c.quote(c.cast_bound_value(value))
198
238
  end
199
239
  end
200
240
 
@@ -54,13 +54,12 @@ module ActiveRecord
54
54
  def define(info, &block) # :nodoc:
55
55
  instance_eval(&block)
56
56
 
57
+ connection.schema_migration.create_table
57
58
  if info[:version].present?
58
- connection.schema_migration.create_table
59
59
  connection.assume_migrated_upto_version(info[:version])
60
60
  end
61
61
 
62
- ActiveRecord::InternalMetadata.create_table
63
- ActiveRecord::InternalMetadata[:environment] = connection.migration_context.current_environment
62
+ connection.internal_metadata.create_table_and_set_flags(connection.migration_context.current_environment)
64
63
  end
65
64
  end
66
65