activerecord 7.0.6 → 7.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1424 -1390
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +15 -16
  5. data/lib/active_record/aggregations.rb +16 -13
  6. data/lib/active_record/association_relation.rb +1 -1
  7. data/lib/active_record/associations/association.rb +18 -3
  8. data/lib/active_record/associations/association_scope.rb +16 -9
  9. data/lib/active_record/associations/belongs_to_association.rb +14 -6
  10. data/lib/active_record/associations/builder/association.rb +3 -3
  11. data/lib/active_record/associations/builder/belongs_to.rb +21 -8
  12. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +1 -5
  13. data/lib/active_record/associations/builder/singular_association.rb +4 -0
  14. data/lib/active_record/associations/collection_association.rb +16 -10
  15. data/lib/active_record/associations/collection_proxy.rb +20 -10
  16. data/lib/active_record/associations/foreign_association.rb +10 -3
  17. data/lib/active_record/associations/has_many_association.rb +20 -13
  18. data/lib/active_record/associations/has_many_through_association.rb +10 -6
  19. data/lib/active_record/associations/has_one_association.rb +10 -7
  20. data/lib/active_record/associations/join_dependency.rb +10 -8
  21. data/lib/active_record/associations/preloader/association.rb +27 -6
  22. data/lib/active_record/associations/preloader.rb +13 -10
  23. data/lib/active_record/associations/singular_association.rb +6 -8
  24. data/lib/active_record/associations/through_association.rb +22 -11
  25. data/lib/active_record/associations.rb +295 -199
  26. data/lib/active_record/attribute_assignment.rb +0 -2
  27. data/lib/active_record/attribute_methods/before_type_cast.rb +17 -0
  28. data/lib/active_record/attribute_methods/dirty.rb +40 -26
  29. data/lib/active_record/attribute_methods/primary_key.rb +76 -24
  30. data/lib/active_record/attribute_methods/query.rb +28 -16
  31. data/lib/active_record/attribute_methods/read.rb +18 -5
  32. data/lib/active_record/attribute_methods/serialization.rb +150 -31
  33. data/lib/active_record/attribute_methods/write.rb +3 -3
  34. data/lib/active_record/attribute_methods.rb +105 -21
  35. data/lib/active_record/attributes.rb +3 -3
  36. data/lib/active_record/autosave_association.rb +60 -18
  37. data/lib/active_record/base.rb +7 -2
  38. data/lib/active_record/callbacks.rb +10 -24
  39. data/lib/active_record/coders/column_serializer.rb +61 -0
  40. data/lib/active_record/coders/json.rb +1 -1
  41. data/lib/active_record/coders/yaml_column.rb +70 -42
  42. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +163 -88
  43. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +2 -0
  44. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +3 -1
  45. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +63 -43
  46. data/lib/active_record/connection_adapters/abstract/database_limits.rb +5 -0
  47. data/lib/active_record/connection_adapters/abstract/database_statements.rb +128 -32
  48. data/lib/active_record/connection_adapters/abstract/query_cache.rb +60 -22
  49. data/lib/active_record/connection_adapters/abstract/quoting.rb +41 -6
  50. data/lib/active_record/connection_adapters/abstract/savepoints.rb +4 -3
  51. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +18 -4
  52. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +137 -11
  53. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +289 -124
  54. data/lib/active_record/connection_adapters/abstract/transaction.rb +287 -58
  55. data/lib/active_record/connection_adapters/abstract_adapter.rb +496 -102
  56. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +214 -113
  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 +23 -144
  60. data/lib/active_record/connection_adapters/mysql/quoting.rb +21 -14
  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 +148 -0
  66. data/lib/active_record/connection_adapters/mysql2_adapter.rb +98 -53
  67. data/lib/active_record/connection_adapters/pool_config.rb +14 -5
  68. data/lib/active_record/connection_adapters/pool_manager.rb +19 -9
  69. data/lib/active_record/connection_adapters/postgresql/column.rb +14 -3
  70. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +71 -40
  71. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +11 -2
  72. data/lib/active_record/connection_adapters/postgresql/oid/timestamp_with_time_zone.rb +1 -1
  73. data/lib/active_record/connection_adapters/postgresql/quoting.rb +15 -8
  74. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +3 -9
  75. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +76 -6
  76. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +131 -2
  77. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +53 -0
  78. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +349 -55
  79. data/lib/active_record/connection_adapters/postgresql_adapter.rb +338 -176
  80. data/lib/active_record/connection_adapters/schema_cache.rb +287 -59
  81. data/lib/active_record/connection_adapters/sqlite3/column.rb +49 -0
  82. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +45 -39
  83. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +9 -5
  84. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +7 -0
  85. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +28 -9
  86. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +210 -83
  87. data/lib/active_record/connection_adapters/statement_pool.rb +7 -0
  88. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +98 -0
  89. data/lib/active_record/connection_adapters/trilogy_adapter.rb +254 -0
  90. data/lib/active_record/connection_adapters.rb +3 -1
  91. data/lib/active_record/connection_handling.rb +71 -94
  92. data/lib/active_record/core.rb +136 -148
  93. data/lib/active_record/counter_cache.rb +46 -25
  94. data/lib/active_record/database_configurations/database_config.rb +9 -3
  95. data/lib/active_record/database_configurations/hash_config.rb +22 -12
  96. data/lib/active_record/database_configurations/url_config.rb +17 -11
  97. data/lib/active_record/database_configurations.rb +86 -33
  98. data/lib/active_record/delegated_type.rb +8 -3
  99. data/lib/active_record/deprecator.rb +7 -0
  100. data/lib/active_record/destroy_association_async_job.rb +2 -0
  101. data/lib/active_record/encryption/auto_filtered_parameters.rb +66 -0
  102. data/lib/active_record/encryption/cipher/aes256_gcm.rb +4 -1
  103. data/lib/active_record/encryption/config.rb +25 -1
  104. data/lib/active_record/encryption/configurable.rb +12 -19
  105. data/lib/active_record/encryption/context.rb +10 -3
  106. data/lib/active_record/encryption/contexts.rb +5 -1
  107. data/lib/active_record/encryption/derived_secret_key_provider.rb +8 -2
  108. data/lib/active_record/encryption/encryptable_record.rb +36 -18
  109. data/lib/active_record/encryption/encrypted_attribute_type.rb +17 -6
  110. data/lib/active_record/encryption/extended_deterministic_queries.rb +66 -54
  111. data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +3 -3
  112. data/lib/active_record/encryption/key_generator.rb +12 -1
  113. data/lib/active_record/encryption/message_serializer.rb +2 -0
  114. data/lib/active_record/encryption/properties.rb +3 -3
  115. data/lib/active_record/encryption/scheme.rb +19 -22
  116. data/lib/active_record/encryption.rb +1 -0
  117. data/lib/active_record/enum.rb +113 -26
  118. data/lib/active_record/errors.rb +108 -15
  119. data/lib/active_record/explain.rb +23 -3
  120. data/lib/active_record/fixture_set/model_metadata.rb +14 -4
  121. data/lib/active_record/fixture_set/render_context.rb +2 -0
  122. data/lib/active_record/fixture_set/table_row.rb +29 -8
  123. data/lib/active_record/fixtures.rb +119 -71
  124. data/lib/active_record/future_result.rb +30 -5
  125. data/lib/active_record/gem_version.rb +3 -3
  126. data/lib/active_record/inheritance.rb +30 -16
  127. data/lib/active_record/insert_all.rb +55 -8
  128. data/lib/active_record/integration.rb +8 -8
  129. data/lib/active_record/internal_metadata.rb +120 -30
  130. data/lib/active_record/locking/pessimistic.rb +5 -2
  131. data/lib/active_record/log_subscriber.rb +29 -12
  132. data/lib/active_record/marshalling.rb +56 -0
  133. data/lib/active_record/message_pack.rb +124 -0
  134. data/lib/active_record/middleware/database_selector/resolver.rb +4 -0
  135. data/lib/active_record/middleware/database_selector.rb +5 -7
  136. data/lib/active_record/middleware/shard_selector.rb +3 -1
  137. data/lib/active_record/migration/command_recorder.rb +104 -5
  138. data/lib/active_record/migration/compatibility.rb +142 -58
  139. data/lib/active_record/migration/default_strategy.rb +23 -0
  140. data/lib/active_record/migration/execution_strategy.rb +19 -0
  141. data/lib/active_record/migration.rb +265 -112
  142. data/lib/active_record/model_schema.rb +60 -40
  143. data/lib/active_record/nested_attributes.rb +21 -3
  144. data/lib/active_record/normalization.rb +159 -0
  145. data/lib/active_record/persistence.rb +187 -35
  146. data/lib/active_record/promise.rb +84 -0
  147. data/lib/active_record/query_cache.rb +3 -21
  148. data/lib/active_record/query_logs.rb +77 -52
  149. data/lib/active_record/query_logs_formatter.rb +41 -0
  150. data/lib/active_record/querying.rb +15 -2
  151. data/lib/active_record/railtie.rb +109 -47
  152. data/lib/active_record/railties/controller_runtime.rb +12 -8
  153. data/lib/active_record/railties/databases.rake +139 -145
  154. data/lib/active_record/railties/job_runtime.rb +23 -0
  155. data/lib/active_record/readonly_attributes.rb +32 -5
  156. data/lib/active_record/reflection.rb +162 -44
  157. data/lib/active_record/relation/batches/batch_enumerator.rb +5 -3
  158. data/lib/active_record/relation/batches.rb +190 -61
  159. data/lib/active_record/relation/calculations.rb +160 -63
  160. data/lib/active_record/relation/delegation.rb +22 -8
  161. data/lib/active_record/relation/finder_methods.rb +77 -16
  162. data/lib/active_record/relation/merger.rb +2 -0
  163. data/lib/active_record/relation/predicate_builder/association_query_value.rb +11 -2
  164. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +4 -6
  165. data/lib/active_record/relation/predicate_builder/relation_handler.rb +5 -1
  166. data/lib/active_record/relation/predicate_builder.rb +27 -16
  167. data/lib/active_record/relation/query_attribute.rb +25 -1
  168. data/lib/active_record/relation/query_methods.rb +378 -70
  169. data/lib/active_record/relation/spawn_methods.rb +18 -1
  170. data/lib/active_record/relation.rb +76 -35
  171. data/lib/active_record/result.rb +19 -5
  172. data/lib/active_record/runtime_registry.rb +10 -1
  173. data/lib/active_record/sanitization.rb +51 -11
  174. data/lib/active_record/schema.rb +2 -3
  175. data/lib/active_record/schema_dumper.rb +46 -7
  176. data/lib/active_record/schema_migration.rb +68 -33
  177. data/lib/active_record/scoping/default.rb +15 -5
  178. data/lib/active_record/scoping/named.rb +2 -2
  179. data/lib/active_record/scoping.rb +2 -1
  180. data/lib/active_record/secure_password.rb +60 -0
  181. data/lib/active_record/secure_token.rb +21 -3
  182. data/lib/active_record/signed_id.rb +7 -5
  183. data/lib/active_record/store.rb +8 -8
  184. data/lib/active_record/suppressor.rb +3 -1
  185. data/lib/active_record/table_metadata.rb +11 -2
  186. data/lib/active_record/tasks/database_tasks.rb +127 -105
  187. data/lib/active_record/tasks/mysql_database_tasks.rb +15 -6
  188. data/lib/active_record/tasks/postgresql_database_tasks.rb +16 -13
  189. data/lib/active_record/tasks/sqlite_database_tasks.rb +15 -7
  190. data/lib/active_record/test_fixtures.rb +113 -96
  191. data/lib/active_record/timestamp.rb +27 -15
  192. data/lib/active_record/token_for.rb +113 -0
  193. data/lib/active_record/touch_later.rb +11 -6
  194. data/lib/active_record/transactions.rb +39 -13
  195. data/lib/active_record/type/adapter_specific_registry.rb +1 -8
  196. data/lib/active_record/type/internal/timezone.rb +7 -2
  197. data/lib/active_record/type/serialized.rb +4 -0
  198. data/lib/active_record/type/time.rb +4 -0
  199. data/lib/active_record/validations/absence.rb +1 -1
  200. data/lib/active_record/validations/numericality.rb +5 -4
  201. data/lib/active_record/validations/presence.rb +5 -28
  202. data/lib/active_record/validations/uniqueness.rb +47 -2
  203. data/lib/active_record/validations.rb +8 -4
  204. data/lib/active_record/version.rb +1 -1
  205. data/lib/active_record.rb +121 -16
  206. data/lib/arel/errors.rb +10 -0
  207. data/lib/arel/factory_methods.rb +4 -0
  208. data/lib/arel/nodes/and.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 +0 -8
  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 +50 -15
  230. data/lib/active_record/connection_adapters/legacy_pool_manager.rb +0 -35
  231. data/lib/active_record/null_relation.rb +0 -63
@@ -3,6 +3,7 @@
3
3
  require "active_support/core_ext/enumerable"
4
4
 
5
5
  module ActiveRecord
6
+ # = Active Record \Calculations
6
7
  module Calculations
7
8
  class ColumnAliasTracker # :nodoc:
8
9
  def initialize(connection)
@@ -71,8 +72,7 @@ module ActiveRecord
71
72
  # of each key would be the #count.
72
73
  #
73
74
  # Article.group(:status, :category).count
74
- # # => {["draft", "business"]=>10, ["draft", "technology"]=>4,
75
- # # ["published", "business"]=>0, ["published", "technology"]=>2}
75
+ # # => {["draft", "business"]=>10, ["draft", "technology"]=>4, ["published", "technology"]=>2}
76
76
  #
77
77
  # If #count is used with {Relation#select}[rdoc-ref:QueryMethods#select], it will count the selected columns:
78
78
  #
@@ -93,6 +93,12 @@ module ActiveRecord
93
93
  end
94
94
  end
95
95
 
96
+ # Same as #count, but performs the query asynchronously and returns an
97
+ # ActiveRecord::Promise.
98
+ def async_count(column_name = nil)
99
+ async.count(column_name)
100
+ end
101
+
96
102
  # Calculates the average value on a given column. Returns +nil+ if there's
97
103
  # no row. See #calculate for examples with options.
98
104
  #
@@ -101,6 +107,12 @@ module ActiveRecord
101
107
  calculate(:average, column_name)
102
108
  end
103
109
 
110
+ # Same as #average, but performs the query asynchronously and returns an
111
+ # ActiveRecord::Promise.
112
+ def async_average(column_name)
113
+ async.average(column_name)
114
+ end
115
+
104
116
  # Calculates the minimum value on a given column. The value is returned
105
117
  # with the same data type of the column, or +nil+ if there's no row. See
106
118
  # #calculate for examples with options.
@@ -110,6 +122,12 @@ module ActiveRecord
110
122
  calculate(:minimum, column_name)
111
123
  end
112
124
 
125
+ # Same as #minimum, but performs the query asynchronously and returns an
126
+ # ActiveRecord::Promise.
127
+ def async_minimum(column_name)
128
+ async.minimum(column_name)
129
+ end
130
+
113
131
  # Calculates the maximum value on a given column. The value is returned
114
132
  # with the same data type of the column, or +nil+ if there's no row. See
115
133
  # #calculate for examples with options.
@@ -119,32 +137,31 @@ module ActiveRecord
119
137
  calculate(:maximum, column_name)
120
138
  end
121
139
 
140
+ # Same as #maximum, but performs the query asynchronously and returns an
141
+ # ActiveRecord::Promise.
142
+ def async_maximum(column_name)
143
+ async.maximum(column_name)
144
+ end
145
+
122
146
  # Calculates the sum of values on a given column. The value is returned
123
147
  # with the same data type of the column, +0+ if there's no row. See
124
148
  # #calculate for examples with options.
125
149
  #
126
150
  # Person.sum(:age) # => 4562
127
- def sum(identity_or_column = nil, &block)
151
+ def sum(initial_value_or_column = 0, &block)
128
152
  if block_given?
129
- values = map(&block)
130
- if identity_or_column.nil? && (values.first.is_a?(Numeric) || values.first(1) == [] || values.first.respond_to?(:coerce))
131
- identity_or_column = 0
132
- end
133
-
134
- if identity_or_column.nil?
135
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
136
- Rails 7.0 has deprecated Enumerable.sum in favor of Ruby's native implementation available since 2.4.
137
- Sum of non-numeric elements requires an initial argument.
138
- MSG
139
- values.inject(:+) || 0
140
- else
141
- values.sum(identity_or_column)
142
- end
153
+ map(&block).sum(initial_value_or_column)
143
154
  else
144
- calculate(:sum, identity_or_column)
155
+ calculate(:sum, initial_value_or_column)
145
156
  end
146
157
  end
147
158
 
159
+ # Same as #sum, but performs the query asynchronously and returns an
160
+ # ActiveRecord::Promise.
161
+ def async_sum(identity_or_column = nil)
162
+ async.sum(identity_or_column)
163
+ end
164
+
148
165
  # This calculates aggregate values in the given column. Methods for #count, #sum, #average,
149
166
  # #minimum, and #maximum have been added as shortcuts.
150
167
  #
@@ -177,10 +194,23 @@ module ActiveRecord
177
194
  # ...
178
195
  # end
179
196
  def calculate(operation, column_name)
197
+ operation = operation.to_s.downcase
198
+
199
+ if @none
200
+ case operation
201
+ when "count", "sum"
202
+ result = group_values.any? ? Hash.new : 0
203
+ return @async ? Promise::Complete.new(result) : result
204
+ when "average", "minimum", "maximum"
205
+ result = group_values.any? ? Hash.new : nil
206
+ return @async ? Promise::Complete.new(result) : result
207
+ end
208
+ end
209
+
180
210
  if has_include?(column_name)
181
211
  relation = apply_join_dependency
182
212
 
183
- if operation.to_s.downcase == "count"
213
+ if operation == "count"
184
214
  unless distinct_value || distinct_select?(column_name || select_for_count)
185
215
  relation.distinct!
186
216
  relation.select_values = [ klass.primary_key || table[Arel.star] ]
@@ -229,31 +259,45 @@ module ActiveRecord
229
259
  # # => ['0', '27761', '173']
230
260
  #
231
261
  # See also #ids.
232
- #
233
262
  def pluck(*column_names)
263
+ return [] if @none
264
+
234
265
  if loaded? && all_attributes?(column_names)
235
- return records.pluck(*column_names)
266
+ result = records.pluck(*column_names)
267
+ if @async
268
+ return Promise::Complete.new(result)
269
+ else
270
+ return result
271
+ end
236
272
  end
237
273
 
238
274
  if has_include?(column_names.first)
239
275
  relation = apply_join_dependency
240
276
  relation.pluck(*column_names)
241
277
  else
242
- klass.disallow_raw_sql!(column_names)
278
+ klass.disallow_raw_sql!(column_names.flatten)
243
279
  columns = arel_columns(column_names)
244
280
  relation = spawn
245
281
  relation.select_values = columns
246
282
  result = skip_query_cache_if_necessary do
247
283
  if where_clause.contradiction?
248
- ActiveRecord::Result.empty
284
+ ActiveRecord::Result.empty(async: @async)
249
285
  else
250
- klass.connection.select_all(relation.arel, "#{klass.name} Pluck")
286
+ klass.connection.select_all(relation.arel, "#{klass.name} Pluck", async: @async)
251
287
  end
252
288
  end
253
- type_cast_pluck_values(result, columns)
289
+ result.then do |result|
290
+ type_cast_pluck_values(result, columns)
291
+ end
254
292
  end
255
293
  end
256
294
 
295
+ # Same as #pluck, but performs the query asynchronously and returns an
296
+ # ActiveRecord::Promise.
297
+ def async_pluck(*column_names)
298
+ async.pluck(*column_names)
299
+ end
300
+
257
301
  # Pick the value(s) from the named column(s) in the current relation.
258
302
  # This is short-hand for <tt>relation.limit(1).pluck(*column_names).first</tt>, and is primarily useful
259
303
  # when you have a relation that's already narrowed down to a single row.
@@ -270,18 +314,61 @@ module ActiveRecord
270
314
  # # => [ 'David', 'david@loudthinking.com' ]
271
315
  def pick(*column_names)
272
316
  if loaded? && all_attributes?(column_names)
273
- return records.pick(*column_names)
317
+ result = records.pick(*column_names)
318
+ return @async ? Promise::Complete.new(result) : result
274
319
  end
275
320
 
276
- limit(1).pluck(*column_names).first
321
+ limit(1).pluck(*column_names).then(&:first)
277
322
  end
278
323
 
279
- # Pluck all the ID's for the relation using the table's primary key
324
+ # Same as #pick, but performs the query asynchronously and returns an
325
+ # ActiveRecord::Promise.
326
+ def async_pick(*column_names)
327
+ async.pick(*column_names)
328
+ end
329
+
330
+ # Returns the base model's ID's for the relation using the table's primary key
280
331
  #
281
332
  # Person.ids # SELECT people.id FROM people
282
- # Person.joins(:companies).ids # SELECT people.id FROM people INNER JOIN companies ON companies.person_id = people.id
333
+ # Person.joins(:companies).ids # SELECT people.id FROM people INNER JOIN companies ON companies.id = people.company_id
283
334
  def ids
284
- pluck primary_key
335
+ primary_key_array = Array(primary_key)
336
+
337
+ if loaded?
338
+ result = records.map do |record|
339
+ if primary_key_array.one?
340
+ record._read_attribute(primary_key_array.first)
341
+ else
342
+ primary_key_array.map { |column| record._read_attribute(column) }
343
+ end
344
+ end
345
+ return @async ? Promise::Complete.new(result) : result
346
+ end
347
+
348
+ if has_include?(primary_key)
349
+ relation = apply_join_dependency.group(*primary_key_array)
350
+ return relation.ids
351
+ end
352
+
353
+ columns = arel_columns(primary_key_array)
354
+ relation = spawn
355
+ relation.select_values = columns
356
+
357
+ result = if relation.where_clause.contradiction?
358
+ ActiveRecord::Result.empty
359
+ else
360
+ skip_query_cache_if_necessary do
361
+ klass.connection.select_all(relation, "#{klass.name} Ids", async: @async)
362
+ end
363
+ end
364
+
365
+ result.then { |result| type_cast_pluck_values(result, columns) }
366
+ end
367
+
368
+ # Same as #ids, but performs the query asynchronously and returns an
369
+ # ActiveRecord::Promise.
370
+ def async_ids
371
+ async.ids
285
372
  end
286
373
 
287
374
  private
@@ -341,6 +428,7 @@ module ActiveRecord
341
428
  # Shortcut when limit is zero.
342
429
  return 0 if limit_value == 0
343
430
 
431
+ relation = self
344
432
  query_builder = build_count_subquery(spawn, column_name, distinct)
345
433
  else
346
434
  # PostgreSQL doesn't like ORDER BY when there are no GROUP BY
@@ -355,15 +443,23 @@ module ActiveRecord
355
443
  query_builder = relation.arel
356
444
  end
357
445
 
358
- result = skip_query_cache_if_necessary { @klass.connection.select_all(query_builder, "#{@klass.name} #{operation.capitalize}") }
359
-
360
- if operation != "count"
361
- type = column.try(:type_caster) ||
362
- lookup_cast_type_from_join_dependencies(column_name.to_s) || Type.default_value
363
- type = type.subtype if Enum::EnumType === type
446
+ query_result = if relation.where_clause.contradiction?
447
+ ActiveRecord::Result.empty
448
+ else
449
+ skip_query_cache_if_necessary do
450
+ @klass.connection.select_all(query_builder, "#{@klass.name} #{operation.capitalize}", async: @async)
451
+ end
364
452
  end
365
453
 
366
- type_cast_calculated_value(result.cast_values.first, operation, type)
454
+ query_result.then do |result|
455
+ if operation != "count"
456
+ type = column.try(:type_caster) ||
457
+ lookup_cast_type_from_join_dependencies(column_name.to_s) || Type.default_value
458
+ type = type.subtype if Enum::EnumType === type
459
+ end
460
+
461
+ type_cast_calculated_value(result.cast_values.first, operation, type)
462
+ end
367
463
  end
368
464
 
369
465
  def execute_grouped_calculation(operation, column_name, distinct) # :nodoc:
@@ -406,39 +502,40 @@ module ActiveRecord
406
502
  relation.group_values = group_fields
407
503
  relation.select_values = select_values
408
504
 
409
- calculated_data = skip_query_cache_if_necessary { @klass.connection.select_all(relation.arel, "#{@klass.name} #{operation.capitalize}") }
505
+ result = skip_query_cache_if_necessary { @klass.connection.select_all(relation.arel, "#{@klass.name} #{operation.capitalize}", async: @async) }
506
+ result.then do |calculated_data|
507
+ if association
508
+ key_ids = calculated_data.collect { |row| row[group_aliases.first] }
509
+ key_records = association.klass.base_class.where(association.klass.base_class.primary_key => key_ids)
510
+ key_records = key_records.index_by(&:id)
511
+ end
410
512
 
411
- if association
412
- key_ids = calculated_data.collect { |row| row[group_aliases.first] }
413
- key_records = association.klass.base_class.where(association.klass.base_class.primary_key => key_ids)
414
- key_records = key_records.index_by(&:id)
415
- end
513
+ key_types = group_columns.each_with_object({}) do |(aliaz, col_name), types|
514
+ types[aliaz] = col_name.try(:type_caster) ||
515
+ type_for(col_name) do
516
+ calculated_data.column_types.fetch(aliaz, Type.default_value)
517
+ end
518
+ end
416
519
 
417
- key_types = group_columns.each_with_object({}) do |(aliaz, col_name), types|
418
- types[aliaz] = col_name.try(:type_caster) ||
419
- type_for(col_name) do
420
- calculated_data.column_types.fetch(aliaz, Type.default_value)
520
+ hash_rows = calculated_data.cast_values(key_types).map! do |row|
521
+ calculated_data.columns.each_with_object({}).with_index do |(col_name, hash), i|
522
+ hash[col_name] = row[i]
421
523
  end
422
- end
423
-
424
- hash_rows = calculated_data.cast_values(key_types).map! do |row|
425
- calculated_data.columns.each_with_object({}).with_index do |(col_name, hash), i|
426
- hash[col_name] = row[i]
427
524
  end
428
- end
429
525
 
430
- if operation != "count"
431
- type = column.try(:type_caster) ||
432
- lookup_cast_type_from_join_dependencies(column_name.to_s) || Type.default_value
433
- type = type.subtype if Enum::EnumType === type
434
- end
526
+ if operation != "count"
527
+ type = column.try(:type_caster) ||
528
+ lookup_cast_type_from_join_dependencies(column_name.to_s) || Type.default_value
529
+ type = type.subtype if Enum::EnumType === type
530
+ end
435
531
 
436
- hash_rows.each_with_object({}) do |row, result|
437
- key = group_aliases.map { |aliaz| row[aliaz] }
438
- key = key.first if key.size == 1
439
- key = key_records[key] if associated
532
+ hash_rows.each_with_object({}) do |row, result|
533
+ key = group_aliases.map { |aliaz| row[aliaz] }
534
+ key = key.first if key.size == 1
535
+ key = key_records[key] if associated
440
536
 
441
- result[key] = type_cast_calculated_value(row[column_alias], operation, type)
537
+ result[key] = type_cast_calculated_value(row[column_alias], operation, type)
538
+ end
442
539
  end
443
540
  end
444
541
 
@@ -5,6 +5,23 @@ require "active_support/core_ext/module/delegation"
5
5
 
6
6
  module ActiveRecord
7
7
  module Delegation # :nodoc:
8
+ class << self
9
+ def delegated_classes
10
+ [
11
+ ActiveRecord::Relation,
12
+ ActiveRecord::Associations::CollectionProxy,
13
+ ActiveRecord::AssociationRelation,
14
+ ActiveRecord::DisableJoinsAssociationRelation,
15
+ ]
16
+ end
17
+
18
+ def uncacheable_methods
19
+ @uncacheable_methods ||= (
20
+ delegated_classes.flat_map(&:public_instance_methods) - ActiveRecord::Relation.public_instance_methods
21
+ ).to_set.freeze
22
+ end
23
+ end
24
+
8
25
  module DelegateCache # :nodoc:
9
26
  def relation_delegate_class(klass)
10
27
  @relation_delegate_cache[klass]
@@ -12,12 +29,7 @@ module ActiveRecord
12
29
 
13
30
  def initialize_relation_delegate_cache
14
31
  @relation_delegate_cache = cache = {}
15
- [
16
- ActiveRecord::Relation,
17
- ActiveRecord::Associations::CollectionProxy,
18
- ActiveRecord::AssociationRelation,
19
- ActiveRecord::DisableJoinsAssociationRelation
20
- ].each do |klass|
32
+ Delegation.delegated_classes.each do |klass|
21
33
  delegate = Class.new(klass) {
22
34
  include ClassSpecificRelation
23
35
  }
@@ -85,7 +97,7 @@ module ActiveRecord
85
97
  # may vary depending on the klass of a relation, so we create a subclass of Relation
86
98
  # for each different klass, and the delegations are compiled into that subclass only.
87
99
 
88
- delegate :to_xml, :encode_with, :length, :each, :join,
100
+ delegate :to_xml, :encode_with, :length, :each, :join, :intersect?,
89
101
  :[], :&, :|, :+, :-, :sample, :reverse, :rotate, :compact, :in_groups, :in_groups_of,
90
102
  :to_sentence, :to_fs, :to_formatted_s, :as_json,
91
103
  :shuffle, :split, :slice, :index, :rindex, to: :records
@@ -104,7 +116,9 @@ module ActiveRecord
104
116
  private
105
117
  def method_missing(method, *args, &block)
106
118
  if @klass.respond_to?(method)
107
- @klass.generate_relation_method(method)
119
+ unless Delegation.uncacheable_methods.include?(method)
120
+ @klass.generate_relation_method(method)
121
+ end
108
122
  scoping { @klass.public_send(method, *args, &block) }
109
123
  else
110
124
  super
@@ -6,7 +6,9 @@ module ActiveRecord
6
6
  module FinderMethods
7
7
  ONE_AS_ONE = "1 AS one"
8
8
 
9
- # Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]).
9
+ # Find by id - This can either be a specific id (ID), a list of ids (ID, ID, ID), or an array of ids ([ID, ID, ID]).
10
+ # `ID` refers to an "identifier". For models with a single-column primary key, `ID` will be a single value,
11
+ # and for models with a composite primary key, it will be an array of values.
10
12
  # If one or more records cannot be found for the requested ids, then ActiveRecord::RecordNotFound will be raised.
11
13
  # If the primary key is an integer, find by id coerces its arguments by using +to_i+.
12
14
  #
@@ -14,10 +16,31 @@ module ActiveRecord
14
16
  # Person.find("1") # returns the object for ID = 1
15
17
  # Person.find("31-sarah") # returns the object for ID = 31
16
18
  # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
17
- # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
19
+ # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17), or with composite primary key [7, 17]
18
20
  # Person.find([1]) # returns an array for the object with ID = 1
19
21
  # Person.where("administrator = 1").order("created_on DESC").find(1)
20
22
  #
23
+ # ==== Find a record for a composite primary key model
24
+ # TravelRoute.primary_key = [:origin, :destination]
25
+ #
26
+ # TravelRoute.find(["Ottawa", "London"])
27
+ # => #<TravelRoute origin: "Ottawa", destination: "London">
28
+ #
29
+ # TravelRoute.find([["Paris", "Montreal"]])
30
+ # => [#<TravelRoute origin: "Paris", destination: "Montreal">]
31
+ #
32
+ # TravelRoute.find(["New York", "Las Vegas"], ["New York", "Portland"])
33
+ # => [
34
+ # #<TravelRoute origin: "New York", destination: "Las Vegas">,
35
+ # #<TravelRoute origin: "New York", destination: "Portland">
36
+ # ]
37
+ #
38
+ # TravelRoute.find([["Berlin", "London"], ["Barcelona", "Lisbon"]])
39
+ # => [
40
+ # #<TravelRoute origin: "Berlin", destination: "London">,
41
+ # #<TravelRoute origin: "Barcelona", destination: "Lisbon">
42
+ # ]
43
+ #
21
44
  # NOTE: The returned records are in the same order as the ids you provide.
22
45
  # If you want the results to be sorted by database, you can use ActiveRecord::QueryMethods#where
23
46
  # method and provide an explicit ActiveRecord::QueryMethods#order option.
@@ -324,6 +347,8 @@ module ActiveRecord
324
347
  # Person.exists?
325
348
  # Person.where(name: 'Spartacus', rating: 4).exists?
326
349
  def exists?(conditions = :none)
350
+ return false if @none
351
+
327
352
  if Base === conditions
328
353
  raise ArgumentError, <<-MSG.squish
329
354
  You are passing an instance of ActiveRecord::Base to `exists?`.
@@ -350,10 +375,20 @@ module ActiveRecord
350
375
  # compared to the records in memory. If the relation is unloaded, an
351
376
  # efficient existence query is performed, as in #exists?.
352
377
  def include?(record)
378
+ # The existing implementation relies on receiving an Active Record instance as the input parameter named record.
379
+ # Any non-Active Record object passed to this implementation is guaranteed to return `false`.
380
+ return false unless record.is_a?(klass)
381
+
353
382
  if loaded? || offset_value || limit_value || having_clause.any?
354
383
  records.include?(record)
355
384
  else
356
- record.is_a?(klass) && exists?(record.id)
385
+ id = if record.class.composite_primary_key?
386
+ record.class.primary_key.zip(record.id).to_h
387
+ else
388
+ record.id
389
+ end
390
+
391
+ exists?(id)
357
392
  end
358
393
  end
359
394
 
@@ -442,10 +477,17 @@ module ActiveRecord
442
477
  def find_with_ids(*ids)
443
478
  raise UnknownPrimaryKey.new(@klass) if primary_key.nil?
444
479
 
445
- expects_array = ids.first.kind_of?(Array)
480
+ expects_array = if klass.composite_primary_key?
481
+ ids.first.first.is_a?(Array)
482
+ else
483
+ ids.first.is_a?(Array)
484
+ end
485
+
446
486
  return [] if expects_array && ids.first.empty?
447
487
 
448
- ids = ids.flatten.compact.uniq
488
+ ids = ids.first if expects_array
489
+
490
+ ids = ids.compact.uniq
449
491
 
450
492
  model_name = @klass.name
451
493
 
@@ -469,7 +511,12 @@ module ActiveRecord
469
511
  MSG
470
512
  end
471
513
 
472
- relation = where(primary_key => id)
514
+ relation = if klass.composite_primary_key?
515
+ where(primary_key.zip(id).to_h)
516
+ else
517
+ where(primary_key => id)
518
+ end
519
+
473
520
  record = relation.take
474
521
 
475
522
  raise_record_not_found_exception!(id, 0, 1) unless record
@@ -480,7 +527,9 @@ module ActiveRecord
480
527
  def find_some(ids)
481
528
  return find_some_ordered(ids) unless order_values.present?
482
529
 
483
- result = where(primary_key => ids).to_a
530
+ relation = where(primary_key => ids)
531
+ relation = relation.select(table[primary_key]) unless select_values.empty?
532
+ result = relation.to_a
484
533
 
485
534
  expected_size =
486
535
  if limit_value && ids.size > limit_value
@@ -504,7 +553,10 @@ module ActiveRecord
504
553
  def find_some_ordered(ids)
505
554
  ids = ids.slice(offset_value || 0, limit_value || ids.size) || []
506
555
 
507
- result = except(:limit, :offset).where(primary_key => ids).records
556
+ relation = except(:limit, :offset)
557
+ relation = relation.where(primary_key => ids)
558
+ relation = relation.select(table[primary_key]) unless select_values.empty?
559
+ result = relation.records
508
560
 
509
561
  if result.size == ids.size
510
562
  result.in_order_of(:id, ids.map { |id| @klass.type_for_attribute(primary_key).cast(id) })
@@ -559,10 +611,10 @@ module ActiveRecord
559
611
  else
560
612
  relation = ordered_relation
561
613
 
562
- if equal?(relation) || has_limit_or_offset?
614
+ if relation.order_values.empty? || relation.has_limit_or_offset?
563
615
  relation.records[-index]
564
616
  else
565
- relation.last(index)[-index]
617
+ relation.reverse_order.offset(index - 1).first
566
618
  end
567
619
  end
568
620
  end
@@ -572,15 +624,24 @@ module ActiveRecord
572
624
  end
573
625
 
574
626
  def ordered_relation
575
- if order_values.empty? && (implicit_order_column || primary_key)
576
- if implicit_order_column && primary_key && implicit_order_column != primary_key
577
- order(table[implicit_order_column].asc, table[primary_key].asc)
578
- else
579
- order(table[implicit_order_column || primary_key].asc)
580
- end
627
+ if order_values.empty? && (implicit_order_column || !query_constraints_list.nil? || primary_key)
628
+ order(_order_columns.map { |column| table[column].asc })
581
629
  else
582
630
  self
583
631
  end
584
632
  end
633
+
634
+ def _order_columns
635
+ oc = []
636
+
637
+ oc << implicit_order_column if implicit_order_column
638
+ oc << query_constraints_list if query_constraints_list
639
+
640
+ if primary_key && query_constraints_list.nil?
641
+ oc << primary_key
642
+ end
643
+
644
+ oc.flatten.uniq.compact
645
+ end
585
646
  end
586
647
  end
@@ -69,6 +69,8 @@ module ActiveRecord
69
69
  end
70
70
  end
71
71
 
72
+ relation.none! if other.null_relation?
73
+
72
74
  merge_select_values
73
75
  merge_multi_values
74
76
  merge_single_values
@@ -9,7 +9,14 @@ module ActiveRecord
9
9
  end
10
10
 
11
11
  def queries
12
- [ associated_table.join_foreign_key => ids ]
12
+ if associated_table.join_foreign_key.is_a?(Array)
13
+ id_list = ids
14
+ id_list = id_list.pluck(primary_key) if id_list.is_a?(Relation)
15
+
16
+ id_list.map { |ids_set| associated_table.join_foreign_key.zip(ids_set).to_h }
17
+ else
18
+ [ associated_table.join_foreign_key => ids ]
19
+ end
13
20
  end
14
21
 
15
22
  private
@@ -25,7 +32,7 @@ module ActiveRecord
25
32
  when Array
26
33
  value.map { |v| convert_to_id(v) }
27
34
  else
28
- convert_to_id(value)
35
+ [convert_to_id(value)]
29
36
  end
30
37
  end
31
38
 
@@ -50,6 +57,8 @@ module ActiveRecord
50
57
  end
51
58
 
52
59
  def convert_to_id(value)
60
+ return primary_key.map { |pk| value.public_send(pk) } if primary_key.is_a?(Array)
61
+
53
62
  if value.respond_to?(primary_key)
54
63
  value.public_send(primary_key)
55
64
  else
@@ -34,19 +34,17 @@ module ActiveRecord
34
34
  end
35
35
 
36
36
  def klass(value)
37
- case value
38
- when Base
37
+ if value.is_a?(Base)
39
38
  value.class
40
- when Relation
39
+ elsif value.is_a?(Relation)
41
40
  value.klass
42
41
  end
43
42
  end
44
43
 
45
44
  def convert_to_id(value)
46
- case value
47
- when Base
45
+ if value.is_a?(Base)
48
46
  value._read_attribute(primary_key(value))
49
- when Relation
47
+ elsif value.is_a?(Relation)
50
48
  value.select(primary_key(value))
51
49
  else
52
50
  value
@@ -9,7 +9,11 @@ module ActiveRecord
9
9
  end
10
10
 
11
11
  if value.select_values.empty?
12
- value = value.select(value.table[value.klass.primary_key])
12
+ if value.klass.composite_primary_key?
13
+ raise ArgumentError, "Cannot map composite primary key #{value.klass.primary_key} to #{attribute.name}"
14
+ else
15
+ value = value.select(value.table[value.klass.primary_key])
16
+ end
13
17
  end
14
18
 
15
19
  attribute.in(value.arel)