activerecord 6.0.0 → 7.2.3

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 (376) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +996 -594
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +34 -34
  5. data/examples/performance.rb +2 -2
  6. data/lib/active_record/aggregations.rb +22 -20
  7. data/lib/active_record/association_relation.rb +22 -12
  8. data/lib/active_record/associations/alias_tracker.rb +41 -30
  9. data/lib/active_record/associations/association.rb +106 -41
  10. data/lib/active_record/associations/association_scope.rb +30 -21
  11. data/lib/active_record/associations/belongs_to_association.rb +69 -14
  12. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +20 -6
  13. data/lib/active_record/associations/builder/association.rb +39 -6
  14. data/lib/active_record/associations/builder/belongs_to.rb +47 -17
  15. data/lib/active_record/associations/builder/collection_association.rb +14 -6
  16. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +3 -10
  17. data/lib/active_record/associations/builder/has_many.rb +7 -3
  18. data/lib/active_record/associations/builder/has_one.rb +13 -16
  19. data/lib/active_record/associations/builder/singular_association.rb +7 -3
  20. data/lib/active_record/associations/collection_association.rb +90 -53
  21. data/lib/active_record/associations/collection_proxy.rb +54 -19
  22. data/lib/active_record/associations/disable_joins_association_scope.rb +59 -0
  23. data/lib/active_record/associations/errors.rb +265 -0
  24. data/lib/active_record/associations/foreign_association.rb +21 -1
  25. data/lib/active_record/associations/has_many_association.rb +41 -10
  26. data/lib/active_record/associations/has_many_through_association.rb +29 -12
  27. data/lib/active_record/associations/has_one_association.rb +33 -9
  28. data/lib/active_record/associations/has_one_through_association.rb +1 -1
  29. data/lib/active_record/associations/join_dependency/join_association.rb +41 -17
  30. data/lib/active_record/associations/join_dependency/join_part.rb +3 -3
  31. data/lib/active_record/associations/join_dependency.rb +97 -54
  32. data/lib/active_record/associations/nested_error.rb +47 -0
  33. data/lib/active_record/associations/preloader/association.rb +237 -54
  34. data/lib/active_record/associations/preloader/batch.rb +48 -0
  35. data/lib/active_record/associations/preloader/branch.rb +153 -0
  36. data/lib/active_record/associations/preloader/through_association.rb +51 -17
  37. data/lib/active_record/associations/preloader.rb +55 -121
  38. data/lib/active_record/associations/singular_association.rb +16 -4
  39. data/lib/active_record/associations/through_association.rb +26 -15
  40. data/lib/active_record/associations.rb +454 -440
  41. data/lib/active_record/asynchronous_queries_tracker.rb +60 -0
  42. data/lib/active_record/attribute_assignment.rb +11 -14
  43. data/lib/active_record/attribute_methods/before_type_cast.rb +36 -11
  44. data/lib/active_record/attribute_methods/composite_primary_key.rb +84 -0
  45. data/lib/active_record/attribute_methods/dirty.rb +75 -34
  46. data/lib/active_record/attribute_methods/primary_key.rb +53 -31
  47. data/lib/active_record/attribute_methods/query.rb +31 -22
  48. data/lib/active_record/attribute_methods/read.rb +16 -17
  49. data/lib/active_record/attribute_methods/serialization.rb +177 -35
  50. data/lib/active_record/attribute_methods/time_zone_conversion.rb +18 -15
  51. data/lib/active_record/attribute_methods/write.rb +16 -28
  52. data/lib/active_record/attribute_methods.rb +227 -100
  53. data/lib/active_record/attributes.rb +94 -56
  54. data/lib/active_record/autosave_association.rb +119 -73
  55. data/lib/active_record/base.rb +31 -21
  56. data/lib/active_record/callbacks.rb +168 -55
  57. data/lib/active_record/coders/column_serializer.rb +61 -0
  58. data/lib/active_record/coders/json.rb +1 -1
  59. data/lib/active_record/coders/yaml_column.rb +70 -25
  60. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +284 -0
  61. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +211 -0
  62. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +79 -0
  63. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +367 -565
  64. data/lib/active_record/connection_adapters/abstract/database_limits.rb +3 -57
  65. data/lib/active_record/connection_adapters/abstract/database_statements.rb +277 -89
  66. data/lib/active_record/connection_adapters/abstract/query_cache.rb +241 -69
  67. data/lib/active_record/connection_adapters/abstract/quoting.rb +122 -134
  68. data/lib/active_record/connection_adapters/abstract/savepoints.rb +4 -3
  69. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +153 -116
  70. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +324 -72
  71. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +17 -4
  72. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +611 -211
  73. data/lib/active_record/connection_adapters/abstract/transaction.rb +425 -82
  74. data/lib/active_record/connection_adapters/abstract_adapter.rb +698 -211
  75. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +464 -239
  76. data/lib/active_record/connection_adapters/column.rb +28 -1
  77. data/lib/active_record/connection_adapters/deduplicable.rb +29 -0
  78. data/lib/active_record/connection_adapters/mysql/column.rb +2 -1
  79. data/lib/active_record/connection_adapters/mysql/database_statements.rb +32 -137
  80. data/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb +1 -2
  81. data/lib/active_record/connection_adapters/mysql/quoting.rb +90 -43
  82. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +41 -7
  83. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +18 -1
  84. data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +13 -4
  85. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +53 -15
  86. data/lib/active_record/connection_adapters/mysql/type_metadata.rb +10 -1
  87. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +152 -0
  88. data/lib/active_record/connection_adapters/mysql2_adapter.rb +127 -63
  89. data/lib/active_record/connection_adapters/pool_config.rb +83 -0
  90. data/lib/active_record/connection_adapters/pool_manager.rb +57 -0
  91. data/lib/active_record/connection_adapters/postgresql/column.rb +54 -2
  92. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +127 -100
  93. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +1 -2
  94. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +9 -5
  95. data/lib/active_record/connection_adapters/postgresql/oid/date.rb +10 -2
  96. data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +15 -2
  97. data/lib/active_record/connection_adapters/postgresql/oid/enum.rb +0 -1
  98. data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +53 -15
  99. data/lib/active_record/connection_adapters/postgresql/oid/interval.rb +49 -0
  100. data/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb +2 -3
  101. data/lib/active_record/connection_adapters/postgresql/oid/macaddr.rb +25 -0
  102. data/lib/active_record/connection_adapters/postgresql/oid/money.rb +5 -4
  103. data/lib/active_record/connection_adapters/postgresql/oid/oid.rb +1 -1
  104. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +2 -3
  105. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +35 -8
  106. data/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb +1 -1
  107. data/lib/active_record/connection_adapters/postgresql/oid/timestamp.rb +15 -0
  108. data/lib/active_record/connection_adapters/postgresql/oid/timestamp_with_time_zone.rb +30 -0
  109. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +18 -6
  110. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +23 -4
  111. data/lib/active_record/connection_adapters/postgresql/oid.rb +4 -0
  112. data/lib/active_record/connection_adapters/postgresql/quoting.rb +139 -106
  113. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +30 -2
  114. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +98 -4
  115. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +176 -4
  116. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +78 -1
  117. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +462 -118
  118. data/lib/active_record/connection_adapters/postgresql/type_metadata.rb +8 -0
  119. data/lib/active_record/connection_adapters/postgresql/utils.rb +9 -11
  120. data/lib/active_record/connection_adapters/postgresql_adapter.rb +585 -295
  121. data/lib/active_record/connection_adapters/schema_cache.rb +399 -60
  122. data/lib/active_record/connection_adapters/sql_type_metadata.rb +8 -0
  123. data/lib/active_record/connection_adapters/sqlite3/column.rb +62 -0
  124. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +99 -48
  125. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +80 -54
  126. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +27 -1
  127. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +20 -0
  128. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +16 -0
  129. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +102 -24
  130. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +425 -174
  131. data/lib/active_record/connection_adapters/statement_pool.rb +7 -1
  132. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +99 -0
  133. data/lib/active_record/connection_adapters/trilogy_adapter.rb +229 -0
  134. data/lib/active_record/connection_adapters.rb +176 -0
  135. data/lib/active_record/connection_handling.rb +243 -115
  136. data/lib/active_record/core.rb +481 -199
  137. data/lib/active_record/counter_cache.rb +69 -32
  138. data/lib/active_record/database_configurations/connection_url_resolver.rb +107 -0
  139. data/lib/active_record/database_configurations/database_config.rb +77 -10
  140. data/lib/active_record/database_configurations/hash_config.rb +148 -26
  141. data/lib/active_record/database_configurations/url_config.rb +44 -45
  142. data/lib/active_record/database_configurations.rb +190 -114
  143. data/lib/active_record/delegated_type.rb +279 -0
  144. data/lib/active_record/deprecator.rb +7 -0
  145. data/lib/active_record/destroy_association_async_job.rb +38 -0
  146. data/lib/active_record/disable_joins_association_relation.rb +39 -0
  147. data/lib/active_record/dynamic_matchers.rb +5 -6
  148. data/lib/active_record/encryption/auto_filtered_parameters.rb +66 -0
  149. data/lib/active_record/encryption/cipher/aes256_gcm.rb +101 -0
  150. data/lib/active_record/encryption/cipher.rb +53 -0
  151. data/lib/active_record/encryption/config.rb +68 -0
  152. data/lib/active_record/encryption/configurable.rb +60 -0
  153. data/lib/active_record/encryption/context.rb +42 -0
  154. data/lib/active_record/encryption/contexts.rb +76 -0
  155. data/lib/active_record/encryption/derived_secret_key_provider.rb +18 -0
  156. data/lib/active_record/encryption/deterministic_key_provider.rb +14 -0
  157. data/lib/active_record/encryption/encryptable_record.rb +230 -0
  158. data/lib/active_record/encryption/encrypted_attribute_type.rb +175 -0
  159. data/lib/active_record/encryption/encrypted_fixtures.rb +38 -0
  160. data/lib/active_record/encryption/encrypting_only_encryptor.rb +12 -0
  161. data/lib/active_record/encryption/encryptor.rb +171 -0
  162. data/lib/active_record/encryption/envelope_encryption_key_provider.rb +55 -0
  163. data/lib/active_record/encryption/errors.rb +15 -0
  164. data/lib/active_record/encryption/extended_deterministic_queries.rb +157 -0
  165. data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +28 -0
  166. data/lib/active_record/encryption/key.rb +28 -0
  167. data/lib/active_record/encryption/key_generator.rb +53 -0
  168. data/lib/active_record/encryption/key_provider.rb +46 -0
  169. data/lib/active_record/encryption/message.rb +33 -0
  170. data/lib/active_record/encryption/message_pack_message_serializer.rb +76 -0
  171. data/lib/active_record/encryption/message_serializer.rb +96 -0
  172. data/lib/active_record/encryption/null_encryptor.rb +25 -0
  173. data/lib/active_record/encryption/properties.rb +76 -0
  174. data/lib/active_record/encryption/read_only_null_encryptor.rb +28 -0
  175. data/lib/active_record/encryption/scheme.rb +100 -0
  176. data/lib/active_record/encryption.rb +58 -0
  177. data/lib/active_record/enum.rb +224 -73
  178. data/lib/active_record/errors.rb +254 -36
  179. data/lib/active_record/explain.rb +30 -17
  180. data/lib/active_record/explain_registry.rb +11 -6
  181. data/lib/active_record/explain_subscriber.rb +2 -2
  182. data/lib/active_record/fixture_set/file.rb +22 -15
  183. data/lib/active_record/fixture_set/model_metadata.rb +15 -6
  184. data/lib/active_record/fixture_set/render_context.rb +3 -1
  185. data/lib/active_record/fixture_set/table_row.rb +88 -16
  186. data/lib/active_record/fixture_set/table_rows.rb +4 -5
  187. data/lib/active_record/fixtures.rb +229 -116
  188. data/lib/active_record/future_result.rb +178 -0
  189. data/lib/active_record/gem_version.rb +4 -4
  190. data/lib/active_record/inheritance.rb +121 -48
  191. data/lib/active_record/insert_all.rb +178 -29
  192. data/lib/active_record/integration.rb +16 -14
  193. data/lib/active_record/internal_metadata.rb +132 -21
  194. data/lib/active_record/legacy_yaml_adapter.rb +3 -36
  195. data/lib/active_record/locking/optimistic.rb +64 -33
  196. data/lib/active_record/locking/pessimistic.rb +21 -8
  197. data/lib/active_record/log_subscriber.rb +61 -30
  198. data/lib/active_record/marshalling.rb +59 -0
  199. data/lib/active_record/message_pack.rb +124 -0
  200. data/lib/active_record/middleware/database_selector/resolver/session.rb +3 -0
  201. data/lib/active_record/middleware/database_selector/resolver.rb +19 -19
  202. data/lib/active_record/middleware/database_selector.rb +25 -13
  203. data/lib/active_record/middleware/shard_selector.rb +62 -0
  204. data/lib/active_record/migration/command_recorder.rb +160 -55
  205. data/lib/active_record/migration/compatibility.rb +286 -43
  206. data/lib/active_record/migration/default_strategy.rb +22 -0
  207. data/lib/active_record/migration/execution_strategy.rb +19 -0
  208. data/lib/active_record/migration/join_table.rb +1 -2
  209. data/lib/active_record/migration/pending_migration_connection.rb +21 -0
  210. data/lib/active_record/migration.rb +421 -193
  211. data/lib/active_record/model_schema.rb +217 -125
  212. data/lib/active_record/nested_attributes.rb +62 -27
  213. data/lib/active_record/no_touching.rb +4 -4
  214. data/lib/active_record/normalization.rb +163 -0
  215. data/lib/active_record/persistence.rb +322 -319
  216. data/lib/active_record/promise.rb +84 -0
  217. data/lib/active_record/query_cache.rb +18 -15
  218. data/lib/active_record/query_logs.rb +193 -0
  219. data/lib/active_record/query_logs_formatter.rb +41 -0
  220. data/lib/active_record/querying.rb +54 -14
  221. data/lib/active_record/railtie.rb +250 -72
  222. data/lib/active_record/railties/console_sandbox.rb +2 -4
  223. data/lib/active_record/railties/controller_runtime.rb +25 -11
  224. data/lib/active_record/railties/databases.rake +312 -197
  225. data/lib/active_record/railties/job_runtime.rb +23 -0
  226. data/lib/active_record/readonly_attributes.rb +45 -3
  227. data/lib/active_record/reflection.rb +389 -146
  228. data/lib/active_record/relation/batches/batch_enumerator.rb +61 -16
  229. data/lib/active_record/relation/batches.rb +214 -73
  230. data/lib/active_record/relation/calculations.rb +379 -124
  231. data/lib/active_record/relation/delegation.rb +36 -23
  232. data/lib/active_record/relation/finder_methods.rb +159 -49
  233. data/lib/active_record/relation/from_clause.rb +5 -1
  234. data/lib/active_record/relation/merger.rb +41 -33
  235. data/lib/active_record/relation/predicate_builder/array_handler.rb +10 -11
  236. data/lib/active_record/relation/predicate_builder/association_query_value.rb +42 -7
  237. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +20 -13
  238. data/lib/active_record/relation/predicate_builder/relation_handler.rb +5 -1
  239. data/lib/active_record/relation/predicate_builder.rb +79 -53
  240. data/lib/active_record/relation/query_attribute.rb +30 -12
  241. data/lib/active_record/relation/query_methods.rb +1156 -279
  242. data/lib/active_record/relation/record_fetch_warning.rb +12 -11
  243. data/lib/active_record/relation/spawn_methods.rb +10 -9
  244. data/lib/active_record/relation/where_clause.rb +100 -66
  245. data/lib/active_record/relation.rb +829 -194
  246. data/lib/active_record/result.rb +76 -56
  247. data/lib/active_record/runtime_registry.rb +71 -13
  248. data/lib/active_record/sanitization.rb +86 -47
  249. data/lib/active_record/schema.rb +39 -23
  250. data/lib/active_record/schema_dumper.rb +140 -33
  251. data/lib/active_record/schema_migration.rb +74 -29
  252. data/lib/active_record/scoping/default.rb +73 -19
  253. data/lib/active_record/scoping/named.rb +10 -28
  254. data/lib/active_record/scoping.rb +65 -35
  255. data/lib/active_record/secure_password.rb +60 -0
  256. data/lib/active_record/secure_token.rb +34 -8
  257. data/lib/active_record/serialization.rb +11 -4
  258. data/lib/active_record/signed_id.rb +138 -0
  259. data/lib/active_record/statement_cache.rb +26 -10
  260. data/lib/active_record/store.rb +19 -14
  261. data/lib/active_record/suppressor.rb +15 -17
  262. data/lib/active_record/table_metadata.rb +46 -36
  263. data/lib/active_record/tasks/database_tasks.rb +371 -205
  264. data/lib/active_record/tasks/mysql_database_tasks.rb +43 -36
  265. data/lib/active_record/tasks/postgresql_database_tasks.rb +54 -41
  266. data/lib/active_record/tasks/sqlite_database_tasks.rb +25 -13
  267. data/lib/active_record/test_databases.rb +5 -4
  268. data/lib/active_record/test_fixtures.rb +189 -104
  269. data/lib/active_record/testing/query_assertions.rb +121 -0
  270. data/lib/active_record/timestamp.rb +35 -25
  271. data/lib/active_record/token_for.rb +123 -0
  272. data/lib/active_record/touch_later.rb +31 -27
  273. data/lib/active_record/transaction.rb +132 -0
  274. data/lib/active_record/transactions.rb +131 -99
  275. data/lib/active_record/translation.rb +3 -5
  276. data/lib/active_record/type/adapter_specific_registry.rb +33 -18
  277. data/lib/active_record/type/hash_lookup_type_map.rb +34 -2
  278. data/lib/active_record/type/internal/timezone.rb +7 -2
  279. data/lib/active_record/type/serialized.rb +11 -6
  280. data/lib/active_record/type/time.rb +14 -0
  281. data/lib/active_record/type/type_map.rb +17 -21
  282. data/lib/active_record/type/unsigned_integer.rb +0 -1
  283. data/lib/active_record/type.rb +7 -2
  284. data/lib/active_record/type_caster/connection.rb +4 -5
  285. data/lib/active_record/type_caster/map.rb +8 -5
  286. data/lib/active_record/validations/absence.rb +1 -1
  287. data/lib/active_record/validations/associated.rb +13 -8
  288. data/lib/active_record/validations/numericality.rb +36 -0
  289. data/lib/active_record/validations/presence.rb +5 -28
  290. data/lib/active_record/validations/uniqueness.rb +88 -18
  291. data/lib/active_record/validations.rb +15 -8
  292. data/lib/active_record/version.rb +1 -1
  293. data/lib/active_record.rb +446 -40
  294. data/lib/arel/alias_predication.rb +1 -1
  295. data/lib/arel/attributes/attribute.rb +4 -8
  296. data/lib/arel/collectors/bind.rb +8 -1
  297. data/lib/arel/collectors/composite.rb +15 -0
  298. data/lib/arel/collectors/sql_string.rb +7 -0
  299. data/lib/arel/collectors/substitute_binds.rb +7 -0
  300. data/lib/arel/crud.rb +30 -22
  301. data/lib/arel/delete_manager.rb +23 -4
  302. data/lib/arel/errors.rb +10 -0
  303. data/lib/arel/factory_methods.rb +4 -0
  304. data/lib/arel/filter_predications.rb +9 -0
  305. data/lib/arel/insert_manager.rb +2 -3
  306. data/lib/arel/nodes/binary.rb +82 -9
  307. data/lib/arel/nodes/bind_param.rb +8 -0
  308. data/lib/arel/nodes/bound_sql_literal.rb +65 -0
  309. data/lib/arel/nodes/casted.rb +22 -10
  310. data/lib/arel/nodes/cte.rb +36 -0
  311. data/lib/arel/nodes/delete_statement.rb +14 -13
  312. data/lib/arel/nodes/equality.rb +6 -9
  313. data/lib/arel/nodes/filter.rb +10 -0
  314. data/lib/arel/nodes/fragments.rb +35 -0
  315. data/lib/arel/nodes/function.rb +1 -0
  316. data/lib/arel/nodes/grouping.rb +3 -0
  317. data/lib/arel/nodes/homogeneous_in.rb +68 -0
  318. data/lib/arel/nodes/in.rb +8 -1
  319. data/lib/arel/nodes/infix_operation.rb +13 -1
  320. data/lib/arel/nodes/insert_statement.rb +2 -2
  321. data/lib/arel/nodes/join_source.rb +1 -1
  322. data/lib/arel/nodes/leading_join.rb +8 -0
  323. data/lib/arel/nodes/{and.rb → nary.rb} +9 -2
  324. data/lib/arel/nodes/node.rb +122 -11
  325. data/lib/arel/nodes/ordering.rb +27 -0
  326. data/lib/arel/nodes/select_core.rb +2 -2
  327. data/lib/arel/nodes/select_statement.rb +2 -2
  328. data/lib/arel/nodes/sql_literal.rb +16 -0
  329. data/lib/arel/nodes/table_alias.rb +11 -3
  330. data/lib/arel/nodes/unary.rb +0 -1
  331. data/lib/arel/nodes/update_statement.rb +11 -4
  332. data/lib/arel/nodes.rb +10 -3
  333. data/lib/arel/predications.rb +31 -28
  334. data/lib/arel/select_manager.rb +18 -9
  335. data/lib/arel/table.rb +21 -10
  336. data/lib/arel/tree_manager.rb +8 -15
  337. data/lib/arel/update_manager.rb +25 -5
  338. data/lib/arel/visitors/dot.rb +94 -90
  339. data/lib/arel/visitors/mysql.rb +34 -6
  340. data/lib/arel/visitors/postgresql.rb +5 -16
  341. data/lib/arel/visitors/sqlite.rb +25 -1
  342. data/lib/arel/visitors/to_sql.rb +227 -81
  343. data/lib/arel/visitors/visitor.rb +2 -3
  344. data/lib/arel/visitors.rb +0 -7
  345. data/lib/arel.rb +37 -15
  346. data/lib/rails/generators/active_record/application_record/USAGE +8 -0
  347. data/lib/rails/generators/active_record/application_record/application_record_generator.rb +0 -1
  348. data/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt +1 -1
  349. data/lib/rails/generators/active_record/migration/migration_generator.rb +1 -0
  350. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +6 -1
  351. data/lib/rails/generators/active_record/migration/templates/migration.rb.tt +4 -4
  352. data/lib/rails/generators/active_record/migration.rb +9 -3
  353. data/lib/rails/generators/active_record/model/USAGE +113 -0
  354. data/lib/rails/generators/active_record/model/model_generator.rb +49 -4
  355. data/lib/rails/generators/active_record/model/templates/abstract_base_class.rb.tt +7 -0
  356. data/lib/rails/generators/active_record/model/templates/model.rb.tt +1 -1
  357. data/lib/rails/generators/active_record/model/templates/module.rb.tt +2 -2
  358. data/lib/rails/generators/active_record/multi_db/multi_db_generator.rb +16 -0
  359. data/lib/rails/generators/active_record/multi_db/templates/multi_db.rb.tt +44 -0
  360. metadata +117 -30
  361. data/lib/active_record/attribute_decorators.rb +0 -90
  362. data/lib/active_record/connection_adapters/connection_specification.rb +0 -297
  363. data/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb +0 -29
  364. data/lib/active_record/define_callbacks.rb +0 -22
  365. data/lib/active_record/null_relation.rb +0 -68
  366. data/lib/active_record/railties/collection_cache_association_loading.rb +0 -34
  367. data/lib/active_record/relation/predicate_builder/base_handler.rb +0 -18
  368. data/lib/active_record/relation/where_clause_factory.rb +0 -33
  369. data/lib/arel/attributes.rb +0 -22
  370. data/lib/arel/visitors/depth_first.rb +0 -204
  371. data/lib/arel/visitors/ibm_db.rb +0 -34
  372. data/lib/arel/visitors/informix.rb +0 -62
  373. data/lib/arel/visitors/mssql.rb +0 -157
  374. data/lib/arel/visitors/oracle.rb +0 -159
  375. data/lib/arel/visitors/oracle12.rb +0 -66
  376. data/lib/arel/visitors/where_sql.rb +0 -23
@@ -3,21 +3,16 @@
3
3
  require "active_record/relation/from_clause"
4
4
  require "active_record/relation/query_attribute"
5
5
  require "active_record/relation/where_clause"
6
- require "active_record/relation/where_clause_factory"
7
- require "active_model/forbidden_attributes_protection"
6
+ require "active_support/core_ext/array/wrap"
8
7
 
9
8
  module ActiveRecord
10
9
  module QueryMethods
11
- extend ActiveSupport::Concern
12
-
13
10
  include ActiveModel::ForbiddenAttributesProtection
14
11
 
15
- # WhereChain objects act as placeholder for queries in which #where does not have any parameter.
16
- # In this case, #where must be chained with #not to return a new relation.
12
+ # +WhereChain+ objects act as placeholder for queries in which +where+ does not have any parameter.
13
+ # In this case, +where+ can be chained to return a new relation.
17
14
  class WhereChain
18
- include ActiveModel::ForbiddenAttributesProtection
19
-
20
- def initialize(scope)
15
+ def initialize(scope) # :nodoc:
21
16
  @scope = scope
22
17
  end
23
18
 
@@ -41,137 +36,289 @@ module ActiveRecord
41
36
  #
42
37
  # User.where.not(name: %w(Ko1 Nobu))
43
38
  # # SELECT * FROM users WHERE name NOT IN ('Ko1', 'Nobu')
39
+ #
40
+ # User.where.not(name: "Jon", role: "admin")
41
+ # # SELECT * FROM users WHERE NOT (name = 'Jon' AND role = 'admin')
42
+ #
43
+ # If there is a non-nil condition on a nullable column in the hash condition, the records that have
44
+ # nil values on the nullable column won't be returned.
45
+ # User.create!(nullable_country: nil)
46
+ # User.where.not(nullable_country: "UK")
47
+ # # SELECT * FROM users WHERE NOT (nullable_country = 'UK')
48
+ # # => []
44
49
  def not(opts, *rest)
45
- opts = sanitize_forbidden_attributes(opts)
50
+ where_clause = @scope.send(:build_where_clause, opts, rest)
46
51
 
47
- where_clause = @scope.send(:where_clause_factory).build(opts, rest)
52
+ @scope.where_clause += where_clause.invert
48
53
 
49
- @scope.references!(PredicateBuilder.references(opts)) if Hash === opts
54
+ @scope
55
+ end
50
56
 
51
- if not_behaves_as_nor?(opts)
52
- ActiveSupport::Deprecation.warn(<<~MSG.squish)
53
- NOT conditions will no longer behave as NOR in Rails 6.1.
54
- To continue using NOR conditions, NOT each conditions manually
55
- (`#{ opts.keys.map { |key| ".where.not(#{key.inspect} => ...)" }.join }`).
56
- MSG
57
- @scope.where_clause += where_clause.invert(:nor)
58
- else
59
- @scope.where_clause += where_clause.invert
57
+ # Returns a new relation with joins and where clause to identify
58
+ # associated relations.
59
+ #
60
+ # For example, posts that are associated to a related author:
61
+ #
62
+ # Post.where.associated(:author)
63
+ # # SELECT "posts".* FROM "posts"
64
+ # # INNER JOIN "authors" ON "authors"."id" = "posts"."author_id"
65
+ # # WHERE "authors"."id" IS NOT NULL
66
+ #
67
+ # Additionally, multiple relations can be combined. This will return posts
68
+ # associated to both an author and any comments:
69
+ #
70
+ # Post.where.associated(:author, :comments)
71
+ # # SELECT "posts".* FROM "posts"
72
+ # # INNER JOIN "authors" ON "authors"."id" = "posts"."author_id"
73
+ # # INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"
74
+ # # WHERE "authors"."id" IS NOT NULL AND "comments"."id" IS NOT NULL
75
+ #
76
+ # You can define join type in the scope and +associated+ will not use `JOIN` by default.
77
+ #
78
+ # Post.left_joins(:author).where.associated(:author)
79
+ # # SELECT "posts".* FROM "posts"
80
+ # # LEFT OUTER JOIN "authors" "authors"."id" = "posts"."author_id"
81
+ # # WHERE "authors"."id" IS NOT NULL
82
+ #
83
+ # Post.left_joins(:comments).where.associated(:author)
84
+ # # SELECT "posts".* FROM "posts"
85
+ # # INNER JOIN "authors" ON "authors"."id" = "posts"."author_id"
86
+ # # LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id"
87
+ # # WHERE "author"."id" IS NOT NULL
88
+ def associated(*associations)
89
+ associations.each do |association|
90
+ reflection = scope_association_reflection(association)
91
+ unless @scope.joins_values.include?(reflection.name) || @scope.left_outer_joins_values.include?(reflection.name)
92
+ @scope.joins!(association)
93
+ end
94
+
95
+ if reflection.options[:class_name]
96
+ self.not(association => { reflection.association_primary_key => nil })
97
+ else
98
+ self.not(reflection.table_name => { reflection.association_primary_key => nil })
99
+ end
100
+ end
101
+
102
+ @scope
103
+ end
104
+
105
+ # Returns a new relation with left outer joins and where clause to identify
106
+ # missing relations.
107
+ #
108
+ # For example, posts that are missing a related author:
109
+ #
110
+ # Post.where.missing(:author)
111
+ # # SELECT "posts".* FROM "posts"
112
+ # # LEFT OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id"
113
+ # # WHERE "authors"."id" IS NULL
114
+ #
115
+ # Additionally, multiple relations can be combined. This will return posts
116
+ # that are missing both an author and any comments:
117
+ #
118
+ # Post.where.missing(:author, :comments)
119
+ # # SELECT "posts".* FROM "posts"
120
+ # # LEFT OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id"
121
+ # # LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id"
122
+ # # WHERE "authors"."id" IS NULL AND "comments"."id" IS NULL
123
+ def missing(*associations)
124
+ associations.each do |association|
125
+ reflection = scope_association_reflection(association)
126
+ @scope.left_outer_joins!(association)
127
+ if reflection.options[:class_name]
128
+ @scope.where!(association => { reflection.association_primary_key => nil })
129
+ else
130
+ @scope.where!(reflection.table_name => { reflection.association_primary_key => nil })
131
+ end
60
132
  end
61
133
 
62
134
  @scope
63
135
  end
64
136
 
65
137
  private
66
- def not_behaves_as_nor?(opts)
67
- opts.is_a?(Hash) && opts.size > 1
138
+ def scope_association_reflection(association)
139
+ reflection = @scope.klass._reflect_on_association(association)
140
+ unless reflection
141
+ raise ArgumentError.new("An association named `:#{association}` does not exist on the model `#{@scope.name}`.")
142
+ end
143
+ reflection
68
144
  end
69
145
  end
70
146
 
147
+ # A wrapper to distinguish CTE joins from other nodes.
148
+ class CTEJoin # :nodoc:
149
+ attr_reader :name
150
+
151
+ def initialize(name)
152
+ @name = name
153
+ end
154
+ end
155
+
71
156
  FROZEN_EMPTY_ARRAY = [].freeze
72
157
  FROZEN_EMPTY_HASH = {}.freeze
73
158
 
74
159
  Relation::VALUE_METHODS.each do |name|
75
- method_name = \
160
+ method_name, default =
76
161
  case name
77
- when *Relation::MULTI_VALUE_METHODS then "#{name}_values"
78
- when *Relation::SINGLE_VALUE_METHODS then "#{name}_value"
79
- when *Relation::CLAUSE_METHODS then "#{name}_clause"
162
+ when *Relation::MULTI_VALUE_METHODS
163
+ ["#{name}_values", "FROZEN_EMPTY_ARRAY"]
164
+ when *Relation::SINGLE_VALUE_METHODS
165
+ ["#{name}_value", name == :create_with ? "FROZEN_EMPTY_HASH" : "nil"]
166
+ when *Relation::CLAUSE_METHODS
167
+ ["#{name}_clause", name == :from ? "Relation::FromClause.empty" : "Relation::WhereClause.empty"]
80
168
  end
169
+
81
170
  class_eval <<-CODE, __FILE__, __LINE__ + 1
82
- def #{method_name} # def includes_values
83
- default = DEFAULT_VALUES[:#{name}] # default = DEFAULT_VALUES[:includes]
84
- @values.fetch(:#{name}, default) # @values.fetch(:includes, default)
85
- end # end
86
-
87
- def #{method_name}=(value) # def includes_values=(value)
88
- assert_mutability! # assert_mutability!
89
- @values[:#{name}] = value # @values[:includes] = value
90
- end # end
171
+ def #{method_name} # def includes_values
172
+ @values.fetch(:#{name}, #{default}) # @values.fetch(:includes, FROZEN_EMPTY_ARRAY)
173
+ end # end
174
+
175
+ def #{method_name}=(value) # def includes_values=(value)
176
+ assert_modifiable! # assert_modifiable!
177
+ @values[:#{name}] = value # @values[:includes] = value
178
+ end # end
91
179
  CODE
92
180
  end
93
181
 
94
182
  alias extensions extending_values
95
183
 
96
- # Specify relationships to be included in the result set. For
97
- # example:
184
+ # Specify associations +args+ to be eager loaded to prevent N + 1 queries.
185
+ # A separate query is performed for each association, unless a join is
186
+ # required by conditions.
187
+ #
188
+ # For example:
98
189
  #
99
- # users = User.includes(:address)
190
+ # users = User.includes(:address).limit(5)
100
191
  # users.each do |user|
101
192
  # user.address.city
102
193
  # end
103
194
  #
104
- # allows you to access the +address+ attribute of the +User+ model without
105
- # firing an additional query. This will often result in a
106
- # performance improvement over a simple join.
195
+ # # SELECT "users".* FROM "users" LIMIT 5
196
+ # # SELECT "addresses".* FROM "addresses" WHERE "addresses"."id" IN (1,2,3,4,5)
197
+ #
198
+ # Instead of loading the 5 addresses with 5 separate queries, all addresses
199
+ # are loaded with a single query.
107
200
  #
108
- # You can also specify multiple relationships, like this:
201
+ # Loading the associations in a separate query will often result in a
202
+ # performance improvement over a simple join, as a join can result in many
203
+ # rows that contain redundant data and it performs poorly at scale.
109
204
  #
110
- # users = User.includes(:address, :friends)
205
+ # You can also specify multiple associations. Each association will result
206
+ # in an additional query:
111
207
  #
112
- # Loading nested relationships is possible using a Hash:
208
+ # User.includes(:address, :friends).to_a
209
+ # # SELECT "users".* FROM "users"
210
+ # # SELECT "addresses".* FROM "addresses" WHERE "addresses"."id" IN (1,2,3,4,5)
211
+ # # SELECT "friends".* FROM "friends" WHERE "friends"."user_id" IN (1,2,3,4,5)
113
212
  #
114
- # users = User.includes(:address, friends: [:address, :followers])
213
+ # Loading nested associations is possible using a Hash:
115
214
  #
116
- # === conditions
215
+ # User.includes(:address, friends: [:address, :followers])
216
+ #
217
+ # === Conditions
117
218
  #
118
219
  # If you want to add string conditions to your included models, you'll have
119
220
  # to explicitly reference them. For example:
120
221
  #
121
- # User.includes(:posts).where('posts.name = ?', 'example')
222
+ # User.includes(:posts).where('posts.name = ?', 'example').to_a
122
223
  #
123
224
  # Will throw an error, but this will work:
124
225
  #
125
- # User.includes(:posts).where('posts.name = ?', 'example').references(:posts)
226
+ # User.includes(:posts).where('posts.name = ?', 'example').references(:posts).to_a
227
+ # # SELECT "users"."id" AS t0_r0, ... FROM "users"
228
+ # # LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"
229
+ # # WHERE "posts"."name" = ? [["name", "example"]]
230
+ #
231
+ # As the <tt>LEFT OUTER JOIN</tt> already contains the posts, the second query for
232
+ # the posts is no longer performed.
126
233
  #
127
234
  # Note that #includes works with association names while #references needs
128
235
  # the actual table name.
129
236
  #
130
- # If you pass the conditions via hash, you don't need to call #references
237
+ # If you pass the conditions via a Hash, you don't need to call #references
131
238
  # explicitly, as #where references the tables for you. For example, this
132
239
  # will work correctly:
133
240
  #
134
241
  # User.includes(:posts).where(posts: { name: 'example' })
242
+ #
243
+ # NOTE: Conditions affect both sides of an association. For example, the
244
+ # above code will return only users that have a post named "example",
245
+ # <em>and will only include posts named "example"</em>, even when a
246
+ # matching user has other additional posts.
135
247
  def includes(*args)
136
- check_if_method_has_arguments!(:includes, args)
248
+ check_if_method_has_arguments!(__callee__, args)
137
249
  spawn.includes!(*args)
138
250
  end
139
251
 
140
252
  def includes!(*args) # :nodoc:
141
- args.reject!(&:blank?)
142
- args.flatten!
143
-
144
253
  self.includes_values |= args
145
254
  self
146
255
  end
147
256
 
148
- # Forces eager loading by performing a LEFT OUTER JOIN on +args+:
257
+ # Specify associations +args+ to be eager loaded using a <tt>LEFT OUTER JOIN</tt>.
258
+ # Performs a single query joining all specified associations. For example:
259
+ #
260
+ # users = User.eager_load(:address).limit(5)
261
+ # users.each do |user|
262
+ # user.address.city
263
+ # end
264
+ #
265
+ # # SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, ... FROM "users"
266
+ # # LEFT OUTER JOIN "addresses" ON "addresses"."id" = "users"."address_id"
267
+ # # LIMIT 5
268
+ #
269
+ # Instead of loading the 5 addresses with 5 separate queries, all addresses
270
+ # are loaded with a single joined query.
149
271
  #
150
- # User.eager_load(:posts)
151
- # # SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, ...
152
- # # FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" =
153
- # # "users"."id"
272
+ # Loading multiple and nested associations is possible using Hashes and Arrays,
273
+ # similar to #includes:
274
+ #
275
+ # User.eager_load(:address, friends: [:address, :followers])
276
+ # # SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, ... FROM "users"
277
+ # # LEFT OUTER JOIN "addresses" ON "addresses"."id" = "users"."address_id"
278
+ # # LEFT OUTER JOIN "friends" ON "friends"."user_id" = "users"."id"
279
+ # # ...
280
+ #
281
+ # NOTE: Loading the associations in a join can result in many rows that
282
+ # contain redundant data and it performs poorly at scale.
154
283
  def eager_load(*args)
155
- check_if_method_has_arguments!(:eager_load, args)
284
+ check_if_method_has_arguments!(__callee__, args)
156
285
  spawn.eager_load!(*args)
157
286
  end
158
287
 
159
288
  def eager_load!(*args) # :nodoc:
160
- self.eager_load_values += args
289
+ self.eager_load_values |= args
161
290
  self
162
291
  end
163
292
 
164
- # Allows preloading of +args+, in the same way that #includes does:
293
+ # Specify associations +args+ to be eager loaded using separate queries.
294
+ # A separate query is performed for each association.
295
+ #
296
+ # users = User.preload(:address).limit(5)
297
+ # users.each do |user|
298
+ # user.address.city
299
+ # end
165
300
  #
166
- # User.preload(:posts)
167
- # # SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3)
301
+ # # SELECT "users".* FROM "users" LIMIT 5
302
+ # # SELECT "addresses".* FROM "addresses" WHERE "addresses"."id" IN (1,2,3,4,5)
303
+ #
304
+ # Instead of loading the 5 addresses with 5 separate queries, all addresses
305
+ # are loaded with a separate query.
306
+ #
307
+ # Loading multiple and nested associations is possible using Hashes and Arrays,
308
+ # similar to #includes:
309
+ #
310
+ # User.preload(:address, friends: [:address, :followers])
311
+ # # SELECT "users".* FROM "users"
312
+ # # SELECT "addresses".* FROM "addresses" WHERE "addresses"."id" IN (1,2,3,4,5)
313
+ # # SELECT "friends".* FROM "friends" WHERE "friends"."user_id" IN (1,2,3,4,5)
314
+ # # SELECT ...
168
315
  def preload(*args)
169
- check_if_method_has_arguments!(:preload, args)
316
+ check_if_method_has_arguments!(__callee__, args)
170
317
  spawn.preload!(*args)
171
318
  end
172
319
 
173
320
  def preload!(*args) # :nodoc:
174
- self.preload_values += args
321
+ self.preload_values |= args
175
322
  self
176
323
  end
177
324
 
@@ -189,7 +336,7 @@ module ActiveRecord
189
336
  end
190
337
 
191
338
  # Use to indicate that the given +table_names+ are referenced by an SQL string,
192
- # and should therefore be JOINed in any query rather than loaded separately.
339
+ # and should therefore be +JOIN+ed in any query rather than loaded separately.
193
340
  # This method only works in conjunction with #includes.
194
341
  # See #includes for more details.
195
342
  #
@@ -199,14 +346,11 @@ module ActiveRecord
199
346
  # User.includes(:posts).where("posts.name = 'foo'").references(:posts)
200
347
  # # Query now knows the string references posts, so adds a JOIN
201
348
  def references(*table_names)
202
- check_if_method_has_arguments!(:references, table_names)
349
+ check_if_method_has_arguments!(__callee__, table_names)
203
350
  spawn.references!(*table_names)
204
351
  end
205
352
 
206
353
  def references!(*table_names) # :nodoc:
207
- table_names.flatten!
208
- table_names.map!(&:to_s)
209
-
210
354
  self.references_values |= table_names
211
355
  self
212
356
  end
@@ -236,10 +380,18 @@ module ActiveRecord
236
380
  # Model.select(:field, :other_field, :and_one_more)
237
381
  # # => [#<Model id: nil, field: "value", other_field: "value", and_one_more: "value">]
238
382
  #
383
+ # The argument also can be a hash of fields and aliases.
384
+ #
385
+ # Model.select(models: { field: :alias, other_field: :other_alias })
386
+ # # => [#<Model id: nil, alias: "value", other_alias: "value">]
387
+ #
388
+ # Model.select(models: [:field, :other_field])
389
+ # # => [#<Model id: nil, field: "value", other_field: "value">]
390
+ #
239
391
  # You can also use one or more strings, which will be used unchanged as SELECT fields.
240
392
  #
241
393
  # Model.select('field AS field_one', 'other_field AS field_two')
242
- # # => [#<Model id: nil, field: "value", other_field: "value">]
394
+ # # => [#<Model id: nil, field_one: "value", field_two: "value">]
243
395
  #
244
396
  # If an alias was specified, it will be accessible from the resulting objects:
245
397
  #
@@ -250,7 +402,7 @@ module ActiveRecord
250
402
  # except +id+ will throw ActiveModel::MissingAttributeError:
251
403
  #
252
404
  # Model.select(:field).first.other_field
253
- # # => ActiveModel::MissingAttributeError: missing attribute: other_field
405
+ # # => ActiveModel::MissingAttributeError: missing attribute 'other_field' for Model
254
406
  def select(*fields)
255
407
  if block_given?
256
408
  if fields.any?
@@ -260,14 +412,112 @@ module ActiveRecord
260
412
  return super()
261
413
  end
262
414
 
263
- raise ArgumentError, "Call `select' with at least one field" if fields.empty?
415
+ check_if_method_has_arguments!(__callee__, fields, "Call `select' with at least one field.")
416
+
417
+ fields = process_select_args(fields)
264
418
  spawn._select!(*fields)
265
419
  end
266
420
 
267
421
  def _select!(*fields) # :nodoc:
268
- fields.reject!(&:blank?)
269
- fields.flatten!
270
- self.select_values += fields
422
+ self.select_values |= fields
423
+ self
424
+ end
425
+
426
+ # Add a Common Table Expression (CTE) that you can then reference within another SELECT statement.
427
+ #
428
+ # Note: CTE's are only supported in MySQL for versions 8.0 and above. You will not be able to
429
+ # use CTE's with MySQL 5.7.
430
+ #
431
+ # Post.with(posts_with_tags: Post.where("tags_count > ?", 0))
432
+ # # => ActiveRecord::Relation
433
+ # # WITH posts_with_tags AS (
434
+ # # SELECT * FROM posts WHERE (tags_count > 0)
435
+ # # )
436
+ # # SELECT * FROM posts
437
+ #
438
+ # You can also pass an array of sub-queries to be joined in a +UNION ALL+.
439
+ #
440
+ # Post.with(posts_with_tags_or_comments: [Post.where("tags_count > ?", 0), Post.where("comments_count > ?", 0)])
441
+ # # => ActiveRecord::Relation
442
+ # # WITH posts_with_tags_or_comments AS (
443
+ # # (SELECT * FROM posts WHERE (tags_count > 0))
444
+ # # UNION ALL
445
+ # # (SELECT * FROM posts WHERE (comments_count > 0))
446
+ # # )
447
+ # # SELECT * FROM posts
448
+ #
449
+ # Once you define Common Table Expression you can use custom +FROM+ value or +JOIN+ to reference it.
450
+ #
451
+ # Post.with(posts_with_tags: Post.where("tags_count > ?", 0)).from("posts_with_tags AS posts")
452
+ # # => ActiveRecord::Relation
453
+ # # WITH posts_with_tags AS (
454
+ # # SELECT * FROM posts WHERE (tags_count > 0)
455
+ # # )
456
+ # # SELECT * FROM posts_with_tags AS posts
457
+ #
458
+ # Post.with(posts_with_tags: Post.where("tags_count > ?", 0)).joins("JOIN posts_with_tags ON posts_with_tags.id = posts.id")
459
+ # # => ActiveRecord::Relation
460
+ # # WITH posts_with_tags AS (
461
+ # # SELECT * FROM posts WHERE (tags_count > 0)
462
+ # # )
463
+ # # SELECT * FROM posts JOIN posts_with_tags ON posts_with_tags.id = posts.id
464
+ #
465
+ # It is recommended to pass a query as ActiveRecord::Relation. If that is not possible
466
+ # and you have verified it is safe for the database, you can pass it as SQL literal
467
+ # using +Arel+.
468
+ #
469
+ # Post.with(popular_posts: Arel.sql("... complex sql to calculate posts popularity ..."))
470
+ #
471
+ # Great caution should be taken to avoid SQL injection vulnerabilities. This method should not
472
+ # be used with unsafe values that include unsanitized input.
473
+ #
474
+ # To add multiple CTEs just pass multiple key-value pairs
475
+ #
476
+ # Post.with(
477
+ # posts_with_comments: Post.where("comments_count > ?", 0),
478
+ # posts_with_tags: Post.where("tags_count > ?", 0)
479
+ # )
480
+ #
481
+ # or chain multiple +.with+ calls
482
+ #
483
+ # Post
484
+ # .with(posts_with_comments: Post.where("comments_count > ?", 0))
485
+ # .with(posts_with_tags: Post.where("tags_count > ?", 0))
486
+ def with(*args)
487
+ raise ArgumentError, "ActiveRecord::Relation#with does not accept a block" if block_given?
488
+ check_if_method_has_arguments!(__callee__, args)
489
+ spawn.with!(*args)
490
+ end
491
+
492
+ # Like #with, but modifies relation in place.
493
+ def with!(*args) # :nodoc:
494
+ args = process_with_args(args)
495
+ self.with_values |= args
496
+ self
497
+ end
498
+
499
+ # Add a recursive Common Table Expression (CTE) that you can then reference within another SELECT statement.
500
+ #
501
+ # Post.with_recursive(post_and_replies: [Post.where(id: 42), Post.joins('JOIN post_and_replies ON posts.in_reply_to_id = post_and_replies.id')])
502
+ # # => ActiveRecord::Relation
503
+ # # WITH post_and_replies AS (
504
+ # # (SELECT * FROM posts WHERE id = 42)
505
+ # # UNION ALL
506
+ # # (SELECT * FROM posts JOIN post_and_replies ON posts.in_reply_to_id = post_and_replies.id)
507
+ # # )
508
+ # # SELECT * FROM posts
509
+ #
510
+ # See `#with` for more information.
511
+ def with_recursive(*args)
512
+ check_if_method_has_arguments!(__callee__, args)
513
+ spawn.with_recursive!(*args)
514
+ end
515
+
516
+ # Like #with_recursive but modifies the relation in place.
517
+ def with_recursive!(*args) # :nodoc:
518
+ args = process_with_args(args)
519
+ self.with_values |= args
520
+ @with_is_recursive = true
271
521
  self
272
522
  end
273
523
 
@@ -282,7 +532,8 @@ module ActiveRecord
282
532
  # This is short-hand for <tt>unscope(:select).select(fields)</tt>.
283
533
  # Note that we're unscoping the entire select statement.
284
534
  def reselect(*args)
285
- check_if_method_has_arguments!(:reselect, args)
535
+ check_if_method_has_arguments!(__callee__, args)
536
+ args = process_select_args(args)
286
537
  spawn.reselect!(*args)
287
538
  end
288
539
 
@@ -313,28 +564,67 @@ module ActiveRecord
313
564
  # User.select([:id, :first_name]).group(:id, :first_name).first(3)
314
565
  # # => [#<User id: 1, first_name: "Bill">, #<User id: 2, first_name: "Earl">, #<User id: 3, first_name: "Beto">]
315
566
  def group(*args)
316
- check_if_method_has_arguments!(:group, args)
567
+ check_if_method_has_arguments!(__callee__, args)
317
568
  spawn.group!(*args)
318
569
  end
319
570
 
320
571
  def group!(*args) # :nodoc:
321
- args.flatten!
322
-
323
572
  self.group_values += args
324
573
  self
325
574
  end
326
575
 
327
- # Allows to specify an order attribute:
576
+ # Allows you to change a previously set group statement.
577
+ #
578
+ # Post.group(:title, :body)
579
+ # # SELECT `posts`.`*` FROM `posts` GROUP BY `posts`.`title`, `posts`.`body`
580
+ #
581
+ # Post.group(:title, :body).regroup(:title)
582
+ # # SELECT `posts`.`*` FROM `posts` GROUP BY `posts`.`title`
583
+ #
584
+ # This is short-hand for <tt>unscope(:group).group(fields)</tt>.
585
+ # Note that we're unscoping the entire group statement.
586
+ def regroup(*args)
587
+ check_if_method_has_arguments!(__callee__, args)
588
+ spawn.regroup!(*args)
589
+ end
590
+
591
+ # Same as #regroup but operates on relation in-place instead of copying.
592
+ def regroup!(*args) # :nodoc:
593
+ self.group_values = args
594
+ self
595
+ end
596
+
597
+ # Applies an <code>ORDER BY</code> clause to a query.
598
+ #
599
+ # #order accepts arguments in one of several formats.
600
+ #
601
+ # === symbols
602
+ #
603
+ # The symbol represents the name of the column you want to order the results by.
328
604
  #
329
605
  # User.order(:name)
330
606
  # # SELECT "users".* FROM "users" ORDER BY "users"."name" ASC
331
607
  #
608
+ # By default, the order is ascending. If you want descending order, you can
609
+ # map the column name symbol to +:desc+.
610
+ #
332
611
  # User.order(email: :desc)
333
612
  # # SELECT "users".* FROM "users" ORDER BY "users"."email" DESC
334
613
  #
614
+ # Multiple columns can be passed this way, and they will be applied in the order specified.
615
+ #
335
616
  # User.order(:name, email: :desc)
336
617
  # # SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC
337
618
  #
619
+ # === strings
620
+ #
621
+ # Strings are passed directly to the database, allowing you to specify
622
+ # simple SQL expressions.
623
+ #
624
+ # This could be a source of SQL injection, so only strings composed of plain
625
+ # column names and simple <code>function(column_name)</code> expressions
626
+ # with optional +ASC+/+DESC+ modifiers are allowed.
627
+ #
338
628
  # User.order('name')
339
629
  # # SELECT "users".* FROM "users" ORDER BY name
340
630
  #
@@ -343,19 +633,93 @@ module ActiveRecord
343
633
  #
344
634
  # User.order('name DESC, email')
345
635
  # # SELECT "users".* FROM "users" ORDER BY name DESC, email
636
+ #
637
+ # === Arel
638
+ #
639
+ # If you need to pass in complicated expressions that you have verified
640
+ # are safe for the database, you can use Arel.
641
+ #
642
+ # User.order(Arel.sql('end_date - start_date'))
643
+ # # SELECT "users".* FROM "users" ORDER BY end_date - start_date
644
+ #
645
+ # Custom query syntax, like JSON columns for PostgreSQL, is supported in this way.
646
+ #
647
+ # User.order(Arel.sql("payload->>'kind'"))
648
+ # # SELECT "users".* FROM "users" ORDER BY payload->>'kind'
346
649
  def order(*args)
347
- check_if_method_has_arguments!(:order, args)
650
+ check_if_method_has_arguments!(__callee__, args) do
651
+ sanitize_order_arguments(args)
652
+ end
348
653
  spawn.order!(*args)
349
654
  end
350
655
 
351
656
  # Same as #order but operates on relation in-place instead of copying.
352
657
  def order!(*args) # :nodoc:
353
- preprocess_order_args(args)
354
-
355
- self.order_values += args
658
+ preprocess_order_args(args) unless args.empty?
659
+ self.order_values |= args
356
660
  self
357
661
  end
358
662
 
663
+ # Applies an <tt>ORDER BY</tt> clause based on a given +column+,
664
+ # ordered and filtered by a specific set of +values+.
665
+ #
666
+ # User.in_order_of(:id, [1, 5, 3])
667
+ # # SELECT "users".* FROM "users"
668
+ # # WHERE "users"."id" IN (1, 5, 3)
669
+ # # ORDER BY CASE
670
+ # # WHEN "users"."id" = 1 THEN 1
671
+ # # WHEN "users"."id" = 5 THEN 2
672
+ # # WHEN "users"."id" = 3 THEN 3
673
+ # # END ASC
674
+ #
675
+ # +column+ can point to an enum column; the actual query generated may be different depending
676
+ # on the database adapter and the column definition.
677
+ #
678
+ # class Conversation < ActiveRecord::Base
679
+ # enum :status, [ :active, :archived ]
680
+ # end
681
+ #
682
+ # Conversation.in_order_of(:status, [:archived, :active])
683
+ # # SELECT "conversations".* FROM "conversations"
684
+ # # WHERE "conversations"."status" IN (1, 0)
685
+ # # ORDER BY CASE
686
+ # # WHEN "conversations"."status" = 1 THEN 1
687
+ # # WHEN "conversations"."status" = 0 THEN 2
688
+ # # END ASC
689
+ #
690
+ # +values+ can also include +nil+.
691
+ #
692
+ # Conversation.in_order_of(:status, [nil, :archived, :active])
693
+ # # SELECT "conversations".* FROM "conversations"
694
+ # # WHERE ("conversations"."status" IN (1, 0) OR "conversations"."status" IS NULL)
695
+ # # ORDER BY CASE
696
+ # # WHEN "conversations"."status" IS NULL THEN 1
697
+ # # WHEN "conversations"."status" = 1 THEN 2
698
+ # # WHEN "conversations"."status" = 0 THEN 3
699
+ # # END ASC
700
+ #
701
+ def in_order_of(column, values)
702
+ klass.disallow_raw_sql!([column], permit: model.adapter_class.column_name_with_order_matcher)
703
+ return spawn.none! if values.empty?
704
+
705
+ references = column_references([column])
706
+ self.references_values |= references unless references.empty?
707
+
708
+ values = values.map { |value| type_caster.type_cast_for_database(column, value) }
709
+ arel_column = column.is_a?(Arel::Nodes::SqlLiteral) ? column : order_column(column.to_s)
710
+
711
+ where_clause =
712
+ if values.include?(nil)
713
+ arel_column.in(values.compact).or(arel_column.eq(nil))
714
+ else
715
+ arel_column.in(values)
716
+ end
717
+
718
+ spawn
719
+ .order!(build_case_for_value_position(arel_column, values))
720
+ .where!(where_clause)
721
+ end
722
+
359
723
  # Replaces any existing order defined on the relation with the specified order.
360
724
  #
361
725
  # User.order('email DESC').reorder('id ASC') # generated SQL has 'ORDER BY id ASC'
@@ -364,16 +728,18 @@ module ActiveRecord
364
728
  #
365
729
  # User.order('email DESC').reorder('id ASC').order('name ASC')
366
730
  #
367
- # generates a query with 'ORDER BY id ASC, name ASC'.
731
+ # generates a query with <tt>ORDER BY id ASC, name ASC</tt>.
368
732
  def reorder(*args)
369
- check_if_method_has_arguments!(:reorder, args)
733
+ check_if_method_has_arguments!(__callee__, args) do
734
+ sanitize_order_arguments(args)
735
+ end
370
736
  spawn.reorder!(*args)
371
737
  end
372
738
 
373
739
  # Same as #reorder but operates on relation in-place instead of copying.
374
740
  def reorder!(*args) # :nodoc:
375
- preprocess_order_args(args) unless args.all?(&:blank?)
376
-
741
+ preprocess_order_args(args)
742
+ args.uniq!
377
743
  self.reordering_value = true
378
744
  self.order_values = args
379
745
  self
@@ -381,7 +747,8 @@ module ActiveRecord
381
747
 
382
748
  VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock,
383
749
  :limit, :offset, :joins, :left_outer_joins, :annotate,
384
- :includes, :from, :readonly, :having, :optimizer_hints])
750
+ :includes, :eager_load, :preload, :from, :readonly,
751
+ :having, :optimizer_hints, :with])
385
752
 
386
753
  # Removes an unwanted relation that is already defined on a chain of relations.
387
754
  # This is useful when passing around chains of relations and would like to
@@ -417,12 +784,11 @@ module ActiveRecord
417
784
  # has_many :comments, -> { unscope(where: :trashed) }
418
785
  #
419
786
  def unscope(*args)
420
- check_if_method_has_arguments!(:unscope, args)
787
+ check_if_method_has_arguments!(__callee__, args)
421
788
  spawn.unscope!(*args)
422
789
  end
423
790
 
424
791
  def unscope!(*args) # :nodoc:
425
- args.flatten!
426
792
  self.unscope_values += args
427
793
 
428
794
  args.each do |scope|
@@ -432,15 +798,15 @@ module ActiveRecord
432
798
  if !VALID_UNSCOPING_VALUES.include?(scope)
433
799
  raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}."
434
800
  end
435
- assert_mutability!
436
- @values[scope] = DEFAULT_VALUES[scope]
801
+ assert_modifiable!
802
+ @values.delete(scope)
437
803
  when Hash
438
804
  scope.each do |key, target_value|
439
805
  if key != :where
440
806
  raise ArgumentError, "Hash arguments in .unscope(*args) must have :where as the key."
441
807
  end
442
808
 
443
- target_values = Array(target_value).map(&:to_s)
809
+ target_values = resolve_arel_attributes(Array.wrap(target_value))
444
810
  self.where_clause = where_clause.except(*target_values)
445
811
  end
446
812
  else
@@ -451,7 +817,7 @@ module ActiveRecord
451
817
  self
452
818
  end
453
819
 
454
- # Performs a joins on +args+. The given symbol(s) should match the name of
820
+ # Performs JOINs on +args+. The given symbol(s) should match the name of
455
821
  # the association(s).
456
822
  #
457
823
  # User.joins(:posts)
@@ -473,29 +839,26 @@ module ActiveRecord
473
839
  # # SELECT "users".*
474
840
  # # FROM "users"
475
841
  # # INNER JOIN "posts" ON "posts"."user_id" = "users"."id"
476
- # # INNER JOIN "comments" "comments_posts"
477
- # # ON "comments_posts"."post_id" = "posts"."id"
842
+ # # INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"
478
843
  #
479
844
  # You can use strings in order to customize your joins:
480
845
  #
481
846
  # User.joins("LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id")
482
847
  # # SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id
483
848
  def joins(*args)
484
- check_if_method_has_arguments!(:joins, args)
849
+ check_if_method_has_arguments!(__callee__, args)
485
850
  spawn.joins!(*args)
486
851
  end
487
852
 
488
853
  def joins!(*args) # :nodoc:
489
- args.compact!
490
- args.flatten!
491
- self.joins_values += args
854
+ self.joins_values |= args
492
855
  self
493
856
  end
494
857
 
495
- # Performs a left outer joins on +args+:
858
+ # Performs LEFT OUTER JOINs on +args+:
496
859
  #
497
860
  # User.left_outer_joins(:posts)
498
- # => SELECT "users".* FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"
861
+ # # SELECT "users".* FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"
499
862
  #
500
863
  def left_outer_joins(*args)
501
864
  check_if_method_has_arguments!(__callee__, args)
@@ -504,9 +867,7 @@ module ActiveRecord
504
867
  alias :left_joins :left_outer_joins
505
868
 
506
869
  def left_outer_joins!(*args) # :nodoc:
507
- args.compact!
508
- args.flatten!
509
- self.left_outer_joins_values += args
870
+ self.left_outer_joins_values |= args
510
871
  self
511
872
  end
512
873
 
@@ -517,7 +878,7 @@ module ActiveRecord
517
878
  # SQL is given as an illustration; the actual query generated may be different depending
518
879
  # on the database adapter.
519
880
  #
520
- # === string
881
+ # === \String
521
882
  #
522
883
  # A single string, without additional arguments, is passed to the query
523
884
  # constructor as an SQL fragment, and used in the where clause of the query.
@@ -529,7 +890,7 @@ module ActiveRecord
529
890
  # to injection attacks if not done properly. As an alternative, it is recommended
530
891
  # to use one of the following methods.
531
892
  #
532
- # === array
893
+ # === \Array
533
894
  #
534
895
  # If an array is passed, then the first element of the array is treated as a template, and
535
896
  # the remaining elements are inserted into the template to generate the condition.
@@ -569,20 +930,20 @@ module ActiveRecord
569
930
  # dependencies on the underlying database. If your code is intended for general consumption,
570
931
  # test with multiple database backends.
571
932
  #
572
- # === hash
933
+ # === \Hash
573
934
  #
574
935
  # #where will also accept a hash condition, in which the keys are fields and the values
575
936
  # are values to be searched for.
576
937
  #
577
938
  # Fields can be symbols or strings. Values can be single values, arrays, or ranges.
578
939
  #
579
- # User.where({ name: "Joe", email: "joe@example.com" })
940
+ # User.where(name: "Joe", email: "joe@example.com")
580
941
  # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'
581
942
  #
582
- # User.where({ name: ["Alice", "Bob"]})
943
+ # User.where(name: ["Alice", "Bob"])
583
944
  # # SELECT * FROM users WHERE name IN ('Alice', 'Bob')
584
945
  #
585
- # User.where({ created_at: (Time.now.midnight - 1.day)..Time.now.midnight })
946
+ # User.where(created_at: (Time.now.midnight - 1.day)..Time.now.midnight)
586
947
  # # SELECT * FROM users WHERE (created_at BETWEEN '2012-06-09 07:00:00.000000' AND '2012-06-10 07:00:00.000000')
587
948
  #
588
949
  # In the case of a belongs_to relationship, an association key can be used
@@ -603,6 +964,12 @@ module ActiveRecord
603
964
  # PriceEstimate.where(estimate_of: treasure)
604
965
  # PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: treasure)
605
966
  #
967
+ # Hash conditions may also be specified in a tuple-like syntax. Hash keys may be
968
+ # an array of columns with an array of tuples as values.
969
+ #
970
+ # Article.where([:author_id, :id] => [[15, 1], [15, 2]])
971
+ # # SELECT * FROM articles WHERE author_id = 15 AND id = 1 OR author_id = 15 AND id = 2
972
+ #
606
973
  # === Joins
607
974
  #
608
975
  # If the relation is the result of a join, you may create a condition which uses any of the
@@ -612,37 +979,49 @@ module ActiveRecord
612
979
  #
613
980
  # For hash conditions, you can either use the table name in the key, or use a sub-hash.
614
981
  #
615
- # User.joins(:posts).where({ "posts.published" => true })
616
- # User.joins(:posts).where({ posts: { published: true } })
982
+ # User.joins(:posts).where("posts.published" => true)
983
+ # User.joins(:posts).where(posts: { published: true })
617
984
  #
618
- # === no argument
985
+ # === No Argument
619
986
  #
620
987
  # If no argument is passed, #where returns a new instance of WhereChain, that
621
- # can be chained with #not to return a new relation that negates the where clause.
988
+ # can be chained with WhereChain#not, WhereChain#missing, or WhereChain#associated.
989
+ #
990
+ # Chaining with WhereChain#not:
622
991
  #
623
992
  # User.where.not(name: "Jon")
624
993
  # # SELECT * FROM users WHERE name != 'Jon'
625
994
  #
626
- # See WhereChain for more details on #not.
995
+ # Chaining with WhereChain#associated:
996
+ #
997
+ # Post.where.associated(:author)
998
+ # # SELECT "posts".* FROM "posts"
999
+ # # INNER JOIN "authors" ON "authors"."id" = "posts"."author_id"
1000
+ # # WHERE "authors"."id" IS NOT NULL
1001
+ #
1002
+ # Chaining with WhereChain#missing:
627
1003
  #
628
- # === blank condition
1004
+ # Post.where.missing(:author)
1005
+ # # SELECT "posts".* FROM "posts"
1006
+ # # LEFT OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id"
1007
+ # # WHERE "authors"."id" IS NULL
1008
+ #
1009
+ # === Blank Condition
629
1010
  #
630
1011
  # If the condition is any blank-ish object, then #where is a no-op and returns
631
1012
  # the current relation.
632
- def where(opts = :chain, *rest)
633
- if :chain == opts
1013
+ def where(*args)
1014
+ if args.empty?
634
1015
  WhereChain.new(spawn)
635
- elsif opts.blank?
1016
+ elsif args.length == 1 && args.first.blank?
636
1017
  self
637
1018
  else
638
- spawn.where!(opts, *rest)
1019
+ spawn.where!(*args)
639
1020
  end
640
1021
  end
641
1022
 
642
1023
  def where!(opts, *rest) # :nodoc:
643
- opts = sanitize_forbidden_attributes(opts)
644
- references!(PredicateBuilder.references(opts)) if Hash === opts
645
- self.where_clause += where_clause_factory.build(opts, rest)
1024
+ self.where_clause += build_where_clause(opts, rest)
646
1025
  self
647
1026
  end
648
1027
 
@@ -660,7 +1039,99 @@ module ActiveRecord
660
1039
  # This is short-hand for <tt>unscope(where: conditions.keys).where(conditions)</tt>.
661
1040
  # Note that unlike reorder, we're only unscoping the named conditions -- not the entire where statement.
662
1041
  def rewhere(conditions)
663
- unscope(where: conditions.keys).where(conditions)
1042
+ return unscope(:where) if conditions.nil?
1043
+
1044
+ scope = spawn
1045
+ where_clause = scope.build_where_clause(conditions)
1046
+
1047
+ scope.unscope!(where: where_clause.extract_attributes)
1048
+ scope.where_clause += where_clause
1049
+ scope
1050
+ end
1051
+
1052
+ # Allows you to invert an entire where clause instead of manually applying conditions.
1053
+ #
1054
+ # class User
1055
+ # scope :active, -> { where(accepted: true, locked: false) }
1056
+ # end
1057
+ #
1058
+ # User.where(accepted: true)
1059
+ # # WHERE `accepted` = 1
1060
+ #
1061
+ # User.where(accepted: true).invert_where
1062
+ # # WHERE `accepted` != 1
1063
+ #
1064
+ # User.active
1065
+ # # WHERE `accepted` = 1 AND `locked` = 0
1066
+ #
1067
+ # User.active.invert_where
1068
+ # # WHERE NOT (`accepted` = 1 AND `locked` = 0)
1069
+ #
1070
+ # Be careful because this inverts all conditions before +invert_where+ call.
1071
+ #
1072
+ # class User
1073
+ # scope :active, -> { where(accepted: true, locked: false) }
1074
+ # scope :inactive, -> { active.invert_where } # Do not attempt it
1075
+ # end
1076
+ #
1077
+ # # It also inverts `where(role: 'admin')` unexpectedly.
1078
+ # User.where(role: 'admin').inactive
1079
+ # # WHERE NOT (`role` = 'admin' AND `accepted` = 1 AND `locked` = 0)
1080
+ #
1081
+ def invert_where
1082
+ spawn.invert_where!
1083
+ end
1084
+
1085
+ def invert_where! # :nodoc:
1086
+ self.where_clause = where_clause.invert
1087
+ self
1088
+ end
1089
+
1090
+ # Checks whether the given relation is structurally compatible with this relation, to determine
1091
+ # if it's possible to use the #and and #or methods without raising an error. Structurally
1092
+ # compatible is defined as: they must be scoping the same model, and they must differ only by
1093
+ # #where (if no #group has been defined) or #having (if a #group is present).
1094
+ #
1095
+ # Post.where("id = 1").structurally_compatible?(Post.where("author_id = 3"))
1096
+ # # => true
1097
+ #
1098
+ # Post.joins(:comments).structurally_compatible?(Post.where("id = 1"))
1099
+ # # => false
1100
+ #
1101
+ def structurally_compatible?(other)
1102
+ structurally_incompatible_values_for(other).empty?
1103
+ end
1104
+
1105
+ # Returns a new relation, which is the logical intersection of this relation and the one passed
1106
+ # as an argument.
1107
+ #
1108
+ # The two relations must be structurally compatible: they must be scoping the same model, and
1109
+ # they must differ only by #where (if no #group has been defined) or #having (if a #group is
1110
+ # present).
1111
+ #
1112
+ # Post.where(id: [1, 2]).and(Post.where(id: [2, 3]))
1113
+ # # SELECT `posts`.* FROM `posts` WHERE `posts`.`id` IN (1, 2) AND `posts`.`id` IN (2, 3)
1114
+ #
1115
+ def and(other)
1116
+ if other.is_a?(Relation)
1117
+ spawn.and!(other)
1118
+ else
1119
+ raise ArgumentError, "You have passed #{other.class.name} object to #and. Pass an ActiveRecord::Relation object instead."
1120
+ end
1121
+ end
1122
+
1123
+ def and!(other) # :nodoc:
1124
+ incompatible_values = structurally_incompatible_values_for(other)
1125
+
1126
+ unless incompatible_values.empty?
1127
+ raise ArgumentError, "Relation passed to #and must be structurally compatible. Incompatible values: #{incompatible_values}"
1128
+ end
1129
+
1130
+ self.where_clause |= other.where_clause
1131
+ self.having_clause |= other.having_clause
1132
+ self.references_values |= other.references_values
1133
+
1134
+ self
664
1135
  end
665
1136
 
666
1137
  # Returns a new relation, which is the logical union of this relation and the one passed as an
@@ -668,29 +1139,33 @@ module ActiveRecord
668
1139
  #
669
1140
  # The two relations must be structurally compatible: they must be scoping the same model, and
670
1141
  # they must differ only by #where (if no #group has been defined) or #having (if a #group is
671
- # present). Neither relation may have a #limit, #offset, or #distinct set.
1142
+ # present).
672
1143
  #
673
1144
  # Post.where("id = 1").or(Post.where("author_id = 3"))
674
1145
  # # SELECT `posts`.* FROM `posts` WHERE ((id = 1) OR (author_id = 3))
675
1146
  #
676
1147
  def or(other)
677
- unless other.is_a? Relation
1148
+ if other.is_a?(Relation)
1149
+ if @none
1150
+ other.spawn
1151
+ else
1152
+ spawn.or!(other)
1153
+ end
1154
+ else
678
1155
  raise ArgumentError, "You have passed #{other.class.name} object to #or. Pass an ActiveRecord::Relation object instead."
679
1156
  end
680
-
681
- spawn.or!(other)
682
1157
  end
683
1158
 
684
1159
  def or!(other) # :nodoc:
685
- incompatible_values = structurally_incompatible_values_for_or(other)
1160
+ incompatible_values = structurally_incompatible_values_for(other)
686
1161
 
687
1162
  unless incompatible_values.empty?
688
1163
  raise ArgumentError, "Relation passed to #or must be structurally compatible. Incompatible values: #{incompatible_values}"
689
1164
  end
690
1165
 
691
- self.where_clause = self.where_clause.or(other.where_clause)
1166
+ self.where_clause = where_clause.or(other.where_clause)
692
1167
  self.having_clause = having_clause.or(other.having_clause)
693
- self.references_values += other.references_values
1168
+ self.references_values |= other.references_values
694
1169
 
695
1170
  self
696
1171
  end
@@ -704,10 +1179,7 @@ module ActiveRecord
704
1179
  end
705
1180
 
706
1181
  def having!(opts, *rest) # :nodoc:
707
- opts = sanitize_forbidden_attributes(opts)
708
- references!(PredicateBuilder.references(opts)) if Hash === opts
709
-
710
- self.having_clause += having_clause_factory.build(opts, rest)
1182
+ self.having_clause += build_having_clause(opts, rest)
711
1183
  self
712
1184
  end
713
1185
 
@@ -791,15 +1263,29 @@ module ActiveRecord
791
1263
  end
792
1264
 
793
1265
  def none! # :nodoc:
794
- where!("1=0").extending!(NullRelation)
1266
+ unless @none
1267
+ where!("1=0")
1268
+ @none = true
1269
+ end
1270
+ self
795
1271
  end
796
1272
 
797
- # Sets readonly attributes for the returned relation. If value is
798
- # true (default), attempting to update a record will result in an error.
1273
+ def null_relation? # :nodoc:
1274
+ @none
1275
+ end
1276
+
1277
+ # Mark a relation as readonly. Attempting to update a record will result in
1278
+ # an error.
799
1279
  #
800
1280
  # users = User.readonly
801
1281
  # users.first.save
802
- # => ActiveRecord::ReadOnlyRecord: User is marked as readonly
1282
+ # # => ActiveRecord::ReadOnlyRecord: User is marked as readonly
1283
+ #
1284
+ # To make a readonly relation writable, pass +false+.
1285
+ #
1286
+ # users.readonly(false)
1287
+ # users.first.save
1288
+ # # => true
803
1289
  def readonly(value = true)
804
1290
  spawn.readonly!(value)
805
1291
  end
@@ -809,6 +1295,21 @@ module ActiveRecord
809
1295
  self
810
1296
  end
811
1297
 
1298
+ # Sets the returned relation to strict_loading mode. This will raise an error
1299
+ # if the record tries to lazily load an association.
1300
+ #
1301
+ # user = User.strict_loading.first
1302
+ # user.comments.to_a
1303
+ # # => ActiveRecord::StrictLoadingViolationError
1304
+ def strict_loading(value = true)
1305
+ spawn.strict_loading!(value)
1306
+ end
1307
+
1308
+ def strict_loading!(value = true) # :nodoc:
1309
+ self.strict_loading_value = value
1310
+ self
1311
+ end
1312
+
812
1313
  # Sets attributes to be used when creating new records from a
813
1314
  # relation object.
814
1315
  #
@@ -837,7 +1338,7 @@ module ActiveRecord
837
1338
  self
838
1339
  end
839
1340
 
840
- # Specifies table from which the records will be fetched. For example:
1341
+ # Specifies the table from which the records will be fetched. For example:
841
1342
  #
842
1343
  # Topic.select('title').from('posts')
843
1344
  # # SELECT title FROM posts
@@ -847,9 +1348,26 @@ module ActiveRecord
847
1348
  # Topic.select('title').from(Topic.approved)
848
1349
  # # SELECT title FROM (SELECT * FROM topics WHERE approved = 't') subquery
849
1350
  #
1351
+ # Passing a second argument (string or symbol), creates the alias for the SQL from clause. Otherwise the alias "subquery" is used:
1352
+ #
850
1353
  # Topic.select('a.title').from(Topic.approved, :a)
851
1354
  # # SELECT a.title FROM (SELECT * FROM topics WHERE approved = 't') a
852
1355
  #
1356
+ # It does not add multiple arguments to the SQL from clause. The last +from+ chained is the one used:
1357
+ #
1358
+ # Topic.select('title').from(Topic.approved).from(Topic.inactive)
1359
+ # # SELECT title FROM (SELECT topics.* FROM topics WHERE topics.active = 'f') subquery
1360
+ #
1361
+ # For multiple arguments for the SQL from clause, you can pass a string with the exact elements in the SQL from list:
1362
+ #
1363
+ # color = "red"
1364
+ # Color
1365
+ # .from("colors c, JSONB_ARRAY_ELEMENTS(colored_things) AS colorvalues(colorvalue)")
1366
+ # .where("colorvalue->>'color' = ?", color)
1367
+ # .select("c.*").to_a
1368
+ # # SELECT c.*
1369
+ # # FROM colors c, JSONB_ARRAY_ELEMENTS(colored_things) AS colorvalues(colorvalue)
1370
+ # # WHERE (colorvalue->>'color' = 'red')
853
1371
  def from(value, subquery_name = nil)
854
1372
  spawn.from!(value, subquery_name)
855
1373
  end
@@ -884,7 +1402,7 @@ module ActiveRecord
884
1402
  #
885
1403
  # The object returned is a relation, which can be further extended.
886
1404
  #
887
- # === Using a module
1405
+ # === Using a \Module
888
1406
  #
889
1407
  # module Pagination
890
1408
  # def page(number)
@@ -899,7 +1417,7 @@ module ActiveRecord
899
1417
  #
900
1418
  # scope = Model.all.extending(Pagination, SomethingElse)
901
1419
  #
902
- # === Using a block
1420
+ # === Using a Block
903
1421
  #
904
1422
  # scope = Model.all.extending do
905
1423
  # def page(number)
@@ -945,13 +1463,11 @@ module ActiveRecord
945
1463
  # Topic.optimizer_hints("SeqScan(topics)", "Parallel(topics 8)")
946
1464
  # # SELECT /*+ SeqScan(topics) Parallel(topics 8) */ "topics".* FROM "topics"
947
1465
  def optimizer_hints(*args)
948
- check_if_method_has_arguments!(:optimizer_hints, args)
1466
+ check_if_method_has_arguments!(__callee__, args)
949
1467
  spawn.optimizer_hints!(*args)
950
1468
  end
951
1469
 
952
1470
  def optimizer_hints!(*args) # :nodoc:
953
- args.flatten!
954
-
955
1471
  self.optimizer_hints_values |= args
956
1472
  self
957
1473
  end
@@ -964,8 +1480,7 @@ module ActiveRecord
964
1480
  end
965
1481
 
966
1482
  def reverse_order! # :nodoc:
967
- orders = order_values.uniq
968
- orders.reject!(&:blank?)
1483
+ orders = order_values.compact_blank
969
1484
  self.order_values = reverse_sql_order(orders)
970
1485
  self
971
1486
  end
@@ -989,8 +1504,10 @@ module ActiveRecord
989
1504
  # # SELECT "users"."name" FROM "users" /* selecting */ /* user */ /* names */
990
1505
  #
991
1506
  # The SQL block comment delimiters, "/*" and "*/", will be added automatically.
1507
+ #
1508
+ # Some escaping is performed, however untrusted user input should not be used.
992
1509
  def annotate(*args)
993
- check_if_method_has_arguments!(:annotate, args)
1510
+ check_if_method_has_arguments!(__callee__, args)
994
1511
  spawn.annotate!(*args)
995
1512
  end
996
1513
 
@@ -1000,9 +1517,62 @@ module ActiveRecord
1000
1517
  self
1001
1518
  end
1002
1519
 
1520
+ # Deduplicate multiple values.
1521
+ def uniq!(name)
1522
+ if values = @values[name]
1523
+ values.uniq! if values.is_a?(Array) && !values.empty?
1524
+ end
1525
+ self
1526
+ end
1527
+
1528
+ # Excludes the specified record (or collection of records) from the resulting
1529
+ # relation. For example:
1530
+ #
1531
+ # Post.excluding(post)
1532
+ # # SELECT "posts".* FROM "posts" WHERE "posts"."id" != 1
1533
+ #
1534
+ # Post.excluding(post_one, post_two)
1535
+ # # SELECT "posts".* FROM "posts" WHERE "posts"."id" NOT IN (1, 2)
1536
+ #
1537
+ # Post.excluding(Post.drafts)
1538
+ # # SELECT "posts".* FROM "posts" WHERE "posts"."id" NOT IN (3, 4, 5)
1539
+ #
1540
+ # This can also be called on associations. As with the above example, either
1541
+ # a single record of collection thereof may be specified:
1542
+ #
1543
+ # post = Post.find(1)
1544
+ # comment = Comment.find(2)
1545
+ # post.comments.excluding(comment)
1546
+ # # SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1 AND "comments"."id" != 2
1547
+ #
1548
+ # This is short-hand for <tt>.where.not(id: post.id)</tt> and <tt>.where.not(id: [post_one.id, post_two.id])</tt>.
1549
+ #
1550
+ # An <tt>ArgumentError</tt> will be raised if either no records are
1551
+ # specified, or if any of the records in the collection (if a collection
1552
+ # is passed in) are not instances of the same model that the relation is
1553
+ # scoping.
1554
+ def excluding(*records)
1555
+ relations = records.extract! { |element| element.is_a?(Relation) }
1556
+ records.flatten!(1)
1557
+ records.compact!
1558
+
1559
+ unless records.all?(klass) && relations.all? { |relation| relation.klass == klass }
1560
+ raise ArgumentError, "You must only pass a single or collection of #{klass.name} objects to ##{__callee__}."
1561
+ end
1562
+
1563
+ spawn.excluding!(records + relations.flat_map(&:ids))
1564
+ end
1565
+ alias :without :excluding
1566
+
1567
+ def excluding!(records) # :nodoc:
1568
+ predicates = [ predicate_builder[primary_key, records].invert ]
1569
+ self.where_clause += Relation::WhereClause.new(predicates)
1570
+ self
1571
+ end
1572
+
1003
1573
  # Returns the Arel object associated with the relation.
1004
1574
  def arel(aliases = nil) # :nodoc:
1005
- @arel ||= build_arel(aliases)
1575
+ @arel ||= with_connection { |c| build_arel(c, aliases) }
1006
1576
  end
1007
1577
 
1008
1578
  def construct_join_dependency(associations, join_type) # :nodoc:
@@ -1020,54 +1590,180 @@ module ActiveRecord
1020
1590
  end
1021
1591
  end
1022
1592
 
1593
+ def build_where_clause(opts, rest = []) # :nodoc:
1594
+ opts = sanitize_forbidden_attributes(opts)
1595
+
1596
+ if opts.is_a?(Array)
1597
+ opts, *rest = opts
1598
+ end
1599
+
1600
+ case opts
1601
+ when String
1602
+ if rest.empty?
1603
+ parts = [Arel.sql(opts)]
1604
+ elsif rest.first.is_a?(Hash) && /:\w+/.match?(opts)
1605
+ parts = [build_named_bound_sql_literal(opts, rest.first)]
1606
+ elsif opts.include?("?")
1607
+ parts = [build_bound_sql_literal(opts, rest)]
1608
+ else
1609
+ parts = [klass.sanitize_sql(rest.empty? ? opts : [opts, *rest])]
1610
+ end
1611
+ when Hash
1612
+ opts = opts.transform_keys do |key|
1613
+ if key.is_a?(Array)
1614
+ key.map { |k| klass.attribute_aliases[k.to_s] || k.to_s }
1615
+ else
1616
+ key = key.to_s
1617
+ klass.attribute_aliases[key] || key
1618
+ end
1619
+ end
1620
+ references = PredicateBuilder.references(opts)
1621
+ self.references_values |= references unless references.empty?
1622
+
1623
+ parts = predicate_builder.build_from_hash(opts) do |table_name|
1624
+ lookup_table_klass_from_join_dependencies(table_name)
1625
+ end
1626
+ when Arel::Nodes::Node
1627
+ parts = [opts]
1628
+ else
1629
+ raise ArgumentError, "Unsupported argument type: #{opts} (#{opts.class})"
1630
+ end
1631
+
1632
+ Relation::WhereClause.new(parts)
1633
+ end
1634
+ alias :build_having_clause :build_where_clause
1635
+
1636
+ def async!
1637
+ @async = true
1638
+ self
1639
+ end
1640
+
1641
+ protected
1642
+ def arel_columns(columns)
1643
+ columns.flat_map do |field|
1644
+ case field
1645
+ when Symbol
1646
+ arel_column(field.to_s) do |attr_name|
1647
+ adapter_class.quote_table_name(attr_name)
1648
+ end
1649
+ when String
1650
+ arel_column(field, &:itself)
1651
+ when Proc
1652
+ field.call
1653
+ when Hash
1654
+ arel_columns_from_hash(field)
1655
+ else
1656
+ field
1657
+ end
1658
+ end
1659
+ end
1660
+
1023
1661
  private
1024
- def assert_mutability!
1025
- raise ImmutableRelation if @loaded
1026
- raise ImmutableRelation if defined?(@arel) && @arel
1662
+ def async
1663
+ spawn.async!
1027
1664
  end
1028
1665
 
1029
- def build_arel(aliases)
1030
- arel = Arel::SelectManager.new(table)
1666
+ def build_named_bound_sql_literal(statement, values)
1667
+ bound_values = values.transform_values do |value|
1668
+ if ActiveRecord::Relation === value
1669
+ Arel.sql(value.to_sql)
1670
+ elsif value.respond_to?(:map) && !value.acts_like?(:string)
1671
+ values = value.map { |v| v.respond_to?(:id_for_database) ? v.id_for_database : v }
1672
+ values.empty? ? nil : values
1673
+ else
1674
+ value = value.id_for_database if value.respond_to?(:id_for_database)
1675
+ value
1676
+ end
1677
+ end
1031
1678
 
1032
- if !joins_values.empty?
1033
- build_joins(arel, joins_values.flatten, aliases)
1034
- elsif !left_outer_joins_values.empty?
1035
- build_left_outer_joins(arel, left_outer_joins_values.flatten, aliases)
1679
+ begin
1680
+ Arel::Nodes::BoundSqlLiteral.new("(#{statement})", nil, bound_values)
1681
+ rescue Arel::BindError => error
1682
+ raise ActiveRecord::PreparedStatementInvalid, error.message
1036
1683
  end
1684
+ end
1037
1685
 
1038
- arel.where(where_clause.ast) unless where_clause.empty?
1039
- arel.having(having_clause.ast) unless having_clause.empty?
1040
- if limit_value
1041
- limit_attribute = ActiveModel::Attribute.with_cast_value(
1042
- "LIMIT",
1043
- connection.sanitize_limit(limit_value),
1044
- Type.default_value,
1045
- )
1046
- arel.take(Arel::Nodes::BindParam.new(limit_attribute))
1686
+ def build_bound_sql_literal(statement, values)
1687
+ bound_values = values.map do |value|
1688
+ if ActiveRecord::Relation === value
1689
+ Arel.sql(value.to_sql)
1690
+ elsif value.respond_to?(:map) && !value.acts_like?(:string)
1691
+ values = value.map { |v| v.respond_to?(:id_for_database) ? v.id_for_database : v }
1692
+ values.empty? ? nil : values
1693
+ else
1694
+ value = value.id_for_database if value.respond_to?(:id_for_database)
1695
+ value
1696
+ end
1047
1697
  end
1048
- if offset_value
1049
- offset_attribute = ActiveModel::Attribute.with_cast_value(
1050
- "OFFSET",
1051
- offset_value.to_i,
1052
- Type.default_value,
1053
- )
1054
- arel.skip(Arel::Nodes::BindParam.new(offset_attribute))
1698
+
1699
+ begin
1700
+ Arel::Nodes::BoundSqlLiteral.new("(#{statement})", bound_values, nil)
1701
+ rescue Arel::BindError => error
1702
+ raise ActiveRecord::PreparedStatementInvalid, error.message
1055
1703
  end
1056
- arel.group(*arel_columns(group_values.uniq.reject(&:blank?))) unless group_values.empty?
1704
+ end
1057
1705
 
1058
- build_order(arel)
1706
+ def lookup_table_klass_from_join_dependencies(table_name)
1707
+ each_join_dependencies do |join|
1708
+ return join.base_klass if table_name == join.table_name
1709
+ end
1710
+ nil
1711
+ end
1059
1712
 
1713
+ def each_join_dependencies(join_dependencies = build_join_dependencies, &block)
1714
+ join_dependencies.each do |join_dependency|
1715
+ join_dependency.each(&block)
1716
+ end
1717
+ end
1718
+
1719
+ def build_join_dependencies
1720
+ joins = joins_values | left_outer_joins_values
1721
+ joins |= eager_load_values unless eager_load_values.empty?
1722
+ joins |= includes_values unless includes_values.empty?
1723
+
1724
+ join_dependencies = []
1725
+ join_dependencies.unshift construct_join_dependency(
1726
+ select_named_joins(joins, join_dependencies), nil
1727
+ )
1728
+ end
1729
+
1730
+ def assert_modifiable!
1731
+ raise UnmodifiableRelation if @loaded || @arel
1732
+ end
1733
+
1734
+ def build_arel(connection, aliases = nil)
1735
+ arel = Arel::SelectManager.new(table)
1736
+
1737
+ build_joins(arel.join_sources, aliases)
1738
+
1739
+ arel.where(where_clause.ast) unless where_clause.empty?
1740
+ arel.having(having_clause.ast) unless having_clause.empty?
1741
+ arel.take(build_cast_value("LIMIT", connection.sanitize_limit(limit_value))) if limit_value
1742
+ arel.skip(build_cast_value("OFFSET", offset_value.to_i)) if offset_value
1743
+ arel.group(*arel_columns(group_values.uniq)) unless group_values.empty?
1744
+
1745
+ build_order(arel)
1746
+ build_with(arel)
1060
1747
  build_select(arel)
1061
1748
 
1062
1749
  arel.optimizer_hints(*optimizer_hints_values) unless optimizer_hints_values.empty?
1063
1750
  arel.distinct(distinct_value)
1064
1751
  arel.from(build_from) unless from_clause.empty?
1065
1752
  arel.lock(lock_value) if lock_value
1066
- arel.comment(*annotate_values) unless annotate_values.empty?
1753
+
1754
+ unless annotate_values.empty?
1755
+ annotates = annotate_values
1756
+ annotates = annotates.uniq if annotates.size > 1
1757
+ arel.comment(*annotates)
1758
+ end
1067
1759
 
1068
1760
  arel
1069
1761
  end
1070
1762
 
1763
+ def build_cast_value(name, value)
1764
+ ActiveModel::Attribute.with_cast_value(name, value, Type.default_value)
1765
+ end
1766
+
1071
1767
  def build_from
1072
1768
  opts = from_clause.value
1073
1769
  name = from_clause.name
@@ -1083,127 +1779,195 @@ module ActiveRecord
1083
1779
  end
1084
1780
  end
1085
1781
 
1086
- def valid_association_list(associations)
1782
+ def select_named_joins(join_names, stashed_joins = nil, &block)
1783
+ cte_joins, associations = join_names.partition do |join_name|
1784
+ Symbol === join_name && with_values.any? { _1.key?(join_name) }
1785
+ end
1786
+
1787
+ cte_joins.each do |cte_name|
1788
+ block&.call(CTEJoin.new(cte_name))
1789
+ end
1790
+
1791
+ select_association_list(associations, stashed_joins, &block)
1792
+ end
1793
+
1794
+ def select_association_list(associations, stashed_joins = nil)
1795
+ result = []
1087
1796
  associations.each do |association|
1088
1797
  case association
1089
1798
  when Hash, Symbol, Array
1090
- # valid
1799
+ result << association
1800
+ when ActiveRecord::Associations::JoinDependency
1801
+ stashed_joins&.<< association
1091
1802
  else
1092
- raise ArgumentError, "only Hash, Symbol and Array are allowed"
1803
+ yield association if block_given?
1093
1804
  end
1094
1805
  end
1806
+ result
1095
1807
  end
1096
1808
 
1097
- def build_left_outer_joins(manager, outer_joins, aliases)
1098
- buckets = Hash.new { |h, k| h[k] = [] }
1099
- buckets[:association_join] = valid_association_list(outer_joins)
1100
- build_join_query(manager, buckets, Arel::Nodes::OuterJoin, aliases)
1101
- end
1102
-
1103
- def build_joins(manager, joins, aliases)
1809
+ def build_join_buckets
1104
1810
  buckets = Hash.new { |h, k| h[k] = [] }
1105
1811
 
1106
1812
  unless left_outer_joins_values.empty?
1107
- left_joins = valid_association_list(left_outer_joins_values.flatten)
1108
- buckets[:stashed_join] << construct_join_dependency(left_joins, Arel::Nodes::OuterJoin)
1109
- end
1813
+ stashed_left_joins = []
1814
+ left_joins = select_named_joins(left_outer_joins_values, stashed_left_joins) do |left_join|
1815
+ if left_join.is_a?(CTEJoin)
1816
+ buckets[:join_node] << build_with_join_node(left_join.name, Arel::Nodes::OuterJoin)
1817
+ else
1818
+ raise ArgumentError, "only Hash, Symbol and Array are allowed"
1819
+ end
1820
+ end
1110
1821
 
1111
- joins.map! do |join|
1112
- if join.is_a?(String)
1113
- table.create_string_join(Arel.sql(join.strip)) unless join.blank?
1822
+ if joins_values.empty?
1823
+ buckets[:named_join] = left_joins
1824
+ buckets[:stashed_join] = stashed_left_joins
1825
+ return buckets, Arel::Nodes::OuterJoin
1114
1826
  else
1115
- join
1827
+ stashed_left_joins.unshift construct_join_dependency(left_joins, Arel::Nodes::OuterJoin)
1116
1828
  end
1117
- end.delete_if(&:blank?).uniq!
1829
+ end
1830
+
1831
+ joins = joins_values.dup
1832
+ if joins.last.is_a?(ActiveRecord::Associations::JoinDependency)
1833
+ stashed_eager_load = joins.pop if joins.last.base_klass == klass
1834
+ end
1835
+
1836
+ joins.each_with_index do |join, i|
1837
+ joins[i] = Arel::Nodes::StringJoin.new(Arel.sql(join.strip)) if join.is_a?(String)
1838
+ end
1118
1839
 
1119
1840
  while joins.first.is_a?(Arel::Nodes::Join)
1120
1841
  join_node = joins.shift
1121
- if join_node.is_a?(Arel::Nodes::StringJoin) && !buckets[:stashed_join].empty?
1842
+ if !join_node.is_a?(Arel::Nodes::LeadingJoin) && (stashed_eager_load || stashed_left_joins)
1122
1843
  buckets[:join_node] << join_node
1123
1844
  else
1124
1845
  buckets[:leading_join] << join_node
1125
1846
  end
1126
1847
  end
1127
1848
 
1128
- joins.each do |join|
1129
- case join
1130
- when Hash, Symbol, Array
1131
- buckets[:association_join] << join
1132
- when ActiveRecord::Associations::JoinDependency
1133
- buckets[:stashed_join] << join
1134
- when Arel::Nodes::Join
1849
+ buckets[:named_join] = select_named_joins(joins, buckets[:stashed_join]) do |join|
1850
+ if join.is_a?(Arel::Nodes::Join)
1135
1851
  buckets[:join_node] << join
1852
+ elsif join.is_a?(CTEJoin)
1853
+ buckets[:join_node] << build_with_join_node(join.name)
1136
1854
  else
1137
1855
  raise "unknown class: %s" % join.class.name
1138
1856
  end
1139
1857
  end
1140
1858
 
1141
- build_join_query(manager, buckets, Arel::Nodes::InnerJoin, aliases)
1859
+ buckets[:stashed_join].concat stashed_left_joins if stashed_left_joins
1860
+ buckets[:stashed_join] << stashed_eager_load if stashed_eager_load
1861
+
1862
+ return buckets, Arel::Nodes::InnerJoin
1142
1863
  end
1143
1864
 
1144
- def build_join_query(manager, buckets, join_type, aliases)
1145
- association_joins = buckets[:association_join]
1146
- stashed_joins = buckets[:stashed_join]
1147
- leading_joins = buckets[:leading_join]
1148
- join_nodes = buckets[:join_node]
1865
+ def build_joins(join_sources, aliases = nil)
1866
+ return join_sources if joins_values.empty? && left_outer_joins_values.empty?
1867
+
1868
+ buckets, join_type = build_join_buckets
1869
+
1870
+ named_joins = buckets[:named_join]
1871
+ stashed_joins = buckets[:stashed_join]
1872
+ leading_joins = buckets[:leading_join]
1873
+ join_nodes = buckets[:join_node]
1149
1874
 
1150
- join_sources = manager.join_sources
1151
1875
  join_sources.concat(leading_joins) unless leading_joins.empty?
1152
1876
 
1153
- unless association_joins.empty? && stashed_joins.empty?
1877
+ unless named_joins.empty? && stashed_joins.empty?
1154
1878
  alias_tracker = alias_tracker(leading_joins + join_nodes, aliases)
1155
- join_dependency = construct_join_dependency(association_joins, join_type)
1156
- join_sources.concat(join_dependency.join_constraints(stashed_joins, alias_tracker))
1879
+ join_dependency = construct_join_dependency(named_joins, join_type)
1880
+ join_sources.concat(join_dependency.join_constraints(stashed_joins, alias_tracker, references_values))
1157
1881
  end
1158
1882
 
1159
1883
  join_sources.concat(join_nodes) unless join_nodes.empty?
1884
+ join_sources
1160
1885
  end
1161
1886
 
1162
1887
  def build_select(arel)
1163
1888
  if select_values.any?
1164
- arel.project(*arel_columns(select_values.uniq))
1165
- elsif klass.ignored_columns.any?
1166
- arel.project(*klass.column_names.map { |field| arel_attribute(field) })
1889
+ arel.project(*arel_columns(select_values))
1890
+ elsif klass.ignored_columns.any? || klass.enumerate_columns_in_select_statements
1891
+ arel.project(*klass.column_names.map { |field| table[field] })
1167
1892
  else
1168
1893
  arel.project(table[Arel.star])
1169
1894
  end
1170
1895
  end
1171
1896
 
1172
- def arel_columns(columns)
1173
- columns.flat_map do |field|
1174
- case field
1175
- when Symbol
1176
- arel_column(field.to_s) do |attr_name|
1177
- connection.quote_table_name(attr_name)
1178
- end
1179
- when String
1180
- arel_column(field, &:itself)
1181
- when Proc
1182
- field.call
1897
+ def build_with(arel)
1898
+ return if with_values.empty?
1899
+
1900
+ with_statements = with_values.map do |with_value|
1901
+ build_with_value_from_hash(with_value)
1902
+ end
1903
+
1904
+ @with_is_recursive ? arel.with(:recursive, with_statements) : arel.with(with_statements)
1905
+ end
1906
+
1907
+ def build_with_value_from_hash(hash)
1908
+ hash.map do |name, value|
1909
+ Arel::Nodes::TableAlias.new(build_with_expression_from_value(value), name)
1910
+ end
1911
+ end
1912
+
1913
+ def build_with_expression_from_value(value, nested = false)
1914
+ case value
1915
+ when Arel::Nodes::SqlLiteral then Arel::Nodes::Grouping.new(value)
1916
+ when ActiveRecord::Relation
1917
+ if nested
1918
+ value.arel.ast
1183
1919
  else
1184
- field
1920
+ value.arel
1921
+ end
1922
+ when Arel::SelectManager then value
1923
+ when Array
1924
+ return build_with_expression_from_value(value.first, false) if value.size == 1
1925
+
1926
+ parts = value.map do |query|
1927
+ build_with_expression_from_value(query, true)
1185
1928
  end
1929
+
1930
+ parts.reduce do |result, value|
1931
+ Arel::Nodes::UnionAll.new(result, value)
1932
+ end
1933
+ else
1934
+ raise ArgumentError, "Unsupported argument type: `#{value}` #{value.class}"
1186
1935
  end
1187
1936
  end
1188
1937
 
1938
+ def build_with_join_node(name, kind = Arel::Nodes::InnerJoin)
1939
+ with_table = Arel::Table.new(name)
1940
+
1941
+ table.join(with_table, kind).on(
1942
+ with_table[klass.model_name.to_s.foreign_key].eq(table[klass.primary_key])
1943
+ ).join_sources.first
1944
+ end
1945
+
1189
1946
  def arel_column(field)
1190
1947
  field = klass.attribute_aliases[field] || field
1191
1948
  from = from_clause.name || from_clause.value
1192
1949
 
1193
1950
  if klass.columns_hash.key?(field) && (!from || table_name_matches?(from))
1194
- arel_attribute(field)
1951
+ table[field]
1952
+ elsif /\A(?<table>(?:\w+\.)?\w+)\.(?<column>\w+)\z/ =~ field
1953
+ self.references_values |= [Arel.sql(table, retryable: true)]
1954
+ predicate_builder.resolve_arel_attribute(table, column) do
1955
+ lookup_table_klass_from_join_dependencies(table)
1956
+ end
1195
1957
  else
1196
1958
  yield field
1197
1959
  end
1198
1960
  end
1199
1961
 
1200
1962
  def table_name_matches?(from)
1201
- /(?:\A|(?<!FROM)\s)(?:\b#{table.name}\b|#{connection.quote_table_name(table.name)})(?!\.)/i.match?(from.to_s)
1963
+ table_name = Regexp.escape(table.name)
1964
+ quoted_table_name = Regexp.escape(adapter_class.quote_table_name(table.name))
1965
+ /(?:\A|(?<!FROM)\s)(?:\b#{table_name}\b|#{quoted_table_name})(?!\.)/i.match?(from.to_s)
1202
1966
  end
1203
1967
 
1204
1968
  def reverse_sql_order(order_query)
1205
1969
  if order_query.empty?
1206
- return [arel_attribute(primary_key).desc] if primary_key
1970
+ return [table[primary_key].desc] if primary_key
1207
1971
  raise IrreversibleOrderError,
1208
1972
  "Relation has no current order and table has no primary key to be used as default order"
1209
1973
  end
@@ -1214,6 +1978,8 @@ module ActiveRecord
1214
1978
  o.desc
1215
1979
  when Arel::Nodes::Ordering
1216
1980
  o.reverse
1981
+ when Arel::Nodes::NodeExpression
1982
+ o.desc
1217
1983
  when String
1218
1984
  if does_not_support_reverse?(o)
1219
1985
  raise IrreversibleOrderError, "Order #{o.inspect} cannot be reversed automatically"
@@ -1240,9 +2006,7 @@ module ActiveRecord
1240
2006
  end
1241
2007
 
1242
2008
  def build_order(arel)
1243
- orders = order_values.uniq
1244
- orders.reject!(&:blank?)
1245
-
2009
+ orders = order_values.compact_blank
1246
2010
  arel.order(*orders) unless orders.empty?
1247
2011
  end
1248
2012
 
@@ -1253,7 +2017,9 @@ module ActiveRecord
1253
2017
  args.each do |arg|
1254
2018
  next unless arg.is_a?(Hash)
1255
2019
  arg.each do |_key, value|
1256
- unless VALID_DIRECTIONS.include?(value)
2020
+ if value.is_a?(Hash)
2021
+ validate_order_args([value])
2022
+ elsif VALID_DIRECTIONS.exclude?(value)
1257
2023
  raise ArgumentError,
1258
2024
  "Direction \"#{value}\" is invalid. Valid directions are: #{VALID_DIRECTIONS.to_a.inspect}"
1259
2025
  end
@@ -1261,23 +2027,20 @@ module ActiveRecord
1261
2027
  end
1262
2028
  end
1263
2029
 
1264
- def preprocess_order_args(order_args)
1265
- order_args.reject!(&:blank?)
1266
- order_args.map! do |arg|
1267
- klass.sanitize_sql_for_order(arg)
1268
- end
1269
- order_args.flatten!
2030
+ def flattened_args(args)
2031
+ args.flat_map { |e| (e.is_a?(Hash) || e.is_a?(Array)) ? flattened_args(e.to_a) : e }
2032
+ end
1270
2033
 
2034
+ def preprocess_order_args(order_args)
1271
2035
  @klass.disallow_raw_sql!(
1272
- order_args.flat_map { |a| a.is_a?(Hash) ? a.keys : a },
1273
- permit: connection.column_name_with_order_matcher
2036
+ flattened_args(order_args),
2037
+ permit: model.adapter_class.column_name_with_order_matcher
1274
2038
  )
1275
2039
 
1276
2040
  validate_order_args(order_args)
1277
2041
 
1278
- references = order_args.grep(String)
1279
- references.map! { |arg| arg =~ /^\W?(\w+)\W?\./ && $1 }.compact!
1280
- references!(references) if references.any?
2042
+ references = column_references(order_args)
2043
+ self.references_values |= references unless references.empty?
1281
2044
 
1282
2045
  # if a symbol is given we prepend the quoted table name
1283
2046
  order_args.map! do |arg|
@@ -1285,26 +2048,100 @@ module ActiveRecord
1285
2048
  when Symbol
1286
2049
  order_column(arg.to_s).asc
1287
2050
  when Hash
1288
- arg.map { |field, dir|
1289
- case field
1290
- when Arel::Nodes::SqlLiteral
1291
- field.send(dir.downcase)
2051
+ arg.map do |key, value|
2052
+ if value.is_a?(Hash)
2053
+ value.map do |field, dir|
2054
+ order_column([key.to_s, field.to_s].join(".")).public_send(dir.downcase)
2055
+ end
1292
2056
  else
1293
- order_column(field.to_s).send(dir.downcase)
2057
+ case key
2058
+ when Arel::Nodes::SqlLiteral, Arel::Nodes::Node, Arel::Attribute
2059
+ key.public_send(value.downcase)
2060
+ else
2061
+ order_column(key.to_s).public_send(value.downcase)
2062
+ end
1294
2063
  end
1295
- }
2064
+ end
1296
2065
  else
1297
2066
  arg
1298
2067
  end
1299
2068
  end.flatten!
1300
2069
  end
1301
2070
 
2071
+ def sanitize_order_arguments(order_args)
2072
+ order_args.map! do |arg|
2073
+ klass.sanitize_sql_for_order(arg)
2074
+ end
2075
+ end
2076
+
2077
+ def column_references(order_args)
2078
+ order_args.flat_map do |arg|
2079
+ case arg
2080
+ when String, Symbol
2081
+ extract_table_name_from(arg)
2082
+ when Hash
2083
+ arg
2084
+ .map do |key, value|
2085
+ case value
2086
+ when Hash
2087
+ key.to_s
2088
+ else
2089
+ extract_table_name_from(key) if key.is_a?(String) || key.is_a?(Symbol)
2090
+ end
2091
+ end
2092
+ when Arel::Attribute
2093
+ arg.relation.name
2094
+ when Arel::Nodes::Ordering
2095
+ if arg.expr.is_a?(Arel::Attribute)
2096
+ arg.expr.relation.name
2097
+ end
2098
+ end
2099
+ end.filter_map { |ref| Arel.sql(ref, retryable: true) if ref }
2100
+ end
2101
+
2102
+ def extract_table_name_from(string)
2103
+ string.match(/^\W?(\w+)\W?\./) && $1
2104
+ end
2105
+
1302
2106
  def order_column(field)
1303
2107
  arel_column(field) do |attr_name|
1304
2108
  if attr_name == "count" && !group_values.empty?
1305
- arel_attribute(attr_name)
2109
+ table[attr_name]
1306
2110
  else
1307
- Arel.sql(connection.quote_table_name(attr_name))
2111
+ Arel.sql(adapter_class.quote_table_name(attr_name), retryable: true)
2112
+ end
2113
+ end
2114
+ end
2115
+
2116
+ def build_case_for_value_position(column, values)
2117
+ node = Arel::Nodes::Case.new
2118
+ values.each.with_index(1) do |value, order|
2119
+ node.when(column.eq(value)).then(order)
2120
+ end
2121
+
2122
+ Arel::Nodes::Ascending.new(node)
2123
+ end
2124
+
2125
+ def resolve_arel_attributes(attrs)
2126
+ attrs.flat_map do |attr|
2127
+ case attr
2128
+ when Arel::Predications
2129
+ attr
2130
+ when Hash
2131
+ attr.flat_map do |table, columns|
2132
+ table = table.to_s
2133
+ Array(columns).map do |column|
2134
+ predicate_builder.resolve_arel_attribute(table, column)
2135
+ end
2136
+ end
2137
+ else
2138
+ attr = attr.to_s
2139
+ if attr.include?(".")
2140
+ table, column = attr.split(".", 2)
2141
+ predicate_builder.resolve_arel_attribute(table, column)
2142
+ else
2143
+ attr
2144
+ end
1308
2145
  end
1309
2146
  end
1310
2147
  end
@@ -1318,42 +2155,82 @@ module ActiveRecord
1318
2155
  # Post.references() # raises an error
1319
2156
  # Post.references([]) # does not raise an error
1320
2157
  #
1321
- # This particular method should be called with a method_name and the args
2158
+ # This particular method should be called with a method_name (__callee__) and the args
1322
2159
  # passed into that method as an input. For example:
1323
2160
  #
1324
2161
  # def references(*args)
1325
- # check_if_method_has_arguments!("references", args)
2162
+ # check_if_method_has_arguments!(__callee__, args)
1326
2163
  # ...
1327
2164
  # end
1328
- def check_if_method_has_arguments!(method_name, args)
2165
+ def check_if_method_has_arguments!(method_name, args, message = nil)
1329
2166
  if args.blank?
1330
- raise ArgumentError, "The method .#{method_name}() must contain arguments."
2167
+ raise ArgumentError, message || "The method .#{method_name}() must contain arguments."
2168
+ else
2169
+ yield args if block_given?
2170
+
2171
+ args.flatten!
2172
+ args.compact_blank!
1331
2173
  end
1332
2174
  end
1333
2175
 
1334
- STRUCTURAL_OR_METHODS = Relation::VALUE_METHODS - [:extending, :where, :having, :unscope, :references]
1335
- def structurally_incompatible_values_for_or(other)
1336
- values = other.values
1337
- STRUCTURAL_OR_METHODS.reject do |method|
1338
- default = DEFAULT_VALUES[method]
1339
- @values.fetch(method, default) == values.fetch(method, default)
2176
+ def process_select_args(fields)
2177
+ fields.flat_map do |field|
2178
+ if field.is_a?(Hash)
2179
+ arel_columns_from_hash(field)
2180
+ else
2181
+ field
2182
+ end
1340
2183
  end
1341
2184
  end
1342
2185
 
1343
- def where_clause_factory
1344
- @where_clause_factory ||= Relation::WhereClauseFactory.new(klass, predicate_builder)
2186
+ def arel_columns_from_hash(fields)
2187
+ fields.flat_map do |key, columns_aliases|
2188
+ case columns_aliases
2189
+ when Hash
2190
+ columns_aliases.map do |column, column_alias|
2191
+ if values[:joins]&.include?(key)
2192
+ references = PredicateBuilder.references({ key.to_s => fields[key] })
2193
+ self.references_values |= references unless references.empty?
2194
+ end
2195
+ arel_column("#{key}.#{column}") do
2196
+ predicate_builder.resolve_arel_attribute(key.to_s, column)
2197
+ end.as(column_alias.to_s)
2198
+ end
2199
+ when Array
2200
+ columns_aliases.map do |column|
2201
+ arel_column("#{key}.#{column}", &:itself)
2202
+ end
2203
+ when String, Symbol
2204
+ arel_column(key.to_s) do
2205
+ predicate_builder.resolve_arel_attribute(klass.table_name, key.to_s)
2206
+ end.as(columns_aliases.to_s)
2207
+ end
2208
+ end
1345
2209
  end
1346
- alias having_clause_factory where_clause_factory
1347
2210
 
1348
- DEFAULT_VALUES = {
1349
- create_with: FROZEN_EMPTY_HASH,
1350
- where: Relation::WhereClause.empty,
1351
- having: Relation::WhereClause.empty,
1352
- from: Relation::FromClause.empty
1353
- }
2211
+ def process_with_args(args)
2212
+ args.flat_map do |arg|
2213
+ raise ArgumentError, "Unsupported argument type: #{arg} #{arg.class}" unless arg.is_a?(Hash)
2214
+ arg.map { |k, v| { k => v } }
2215
+ end
2216
+ end
2217
+
2218
+ STRUCTURAL_VALUE_METHODS = (
2219
+ Relation::VALUE_METHODS -
2220
+ [:extending, :where, :having, :unscope, :references, :annotate, :optimizer_hints]
2221
+ ).freeze # :nodoc:
1354
2222
 
1355
- Relation::MULTI_VALUE_METHODS.each do |value|
1356
- DEFAULT_VALUES[value] ||= FROZEN_EMPTY_ARRAY
2223
+ def structurally_incompatible_values_for(other)
2224
+ values = other.values
2225
+ STRUCTURAL_VALUE_METHODS.reject do |method|
2226
+ v1, v2 = @values[method], values[method]
2227
+ if v1.is_a?(Array)
2228
+ next true unless v2.is_a?(Array)
2229
+ v1 = v1.uniq
2230
+ v2 = v2.uniq
2231
+ end
2232
+ v1 == v2
2233
+ end
1357
2234
  end
1358
2235
  end
1359
2236
  end