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
@@ -0,0 +1,279 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/string/inquiry"
4
+
5
+ module ActiveRecord
6
+ # = Delegated types
7
+ #
8
+ # Class hierarchies can map to relational database tables in many ways. Active Record, for example, offers
9
+ # purely abstract classes, where the superclass doesn't persist any attributes, and single-table inheritance,
10
+ # where all attributes from all levels of the hierarchy are represented in a single table. Both have their
11
+ # places, but neither are without their drawbacks.
12
+ #
13
+ # The problem with purely abstract classes is that all concrete subclasses must persist all the shared
14
+ # attributes themselves in their own tables (also known as class-table inheritance). This makes it hard to
15
+ # do queries across the hierarchy. For example, imagine you have the following hierarchy:
16
+ #
17
+ # Entry < ApplicationRecord
18
+ # Message < Entry
19
+ # Comment < Entry
20
+ #
21
+ # How do you show a feed that has both +Message+ and +Comment+ records, which can be easily paginated?
22
+ # Well, you can't! Messages are backed by a messages table and comments by a comments table. You can't
23
+ # pull from both tables at once and use a consistent OFFSET/LIMIT scheme.
24
+ #
25
+ # You can get around the pagination problem by using single-table inheritance, but now you're forced into
26
+ # a single mega table with all the attributes from all subclasses. No matter how divergent. If a Message
27
+ # has a subject, but the comment does not, well, now the comment does anyway! So STI works best when there's
28
+ # little divergence between the subclasses and their attributes.
29
+ #
30
+ # But there's a third way: Delegated types. With this approach, the "superclass" is a concrete class
31
+ # that is represented by its own table, where all the superclass attributes that are shared amongst all the
32
+ # "subclasses" are stored. And then each of the subclasses have their own individual tables for additional
33
+ # attributes that are particular to their implementation. This is similar to what's called multi-table
34
+ # inheritance in Django, but instead of actual inheritance, this approach uses delegation to form the
35
+ # hierarchy and share responsibilities.
36
+ #
37
+ # Let's look at that entry/message/comment example using delegated types:
38
+ #
39
+ # # Schema: entries[ id, account_id, creator_id, entryable_type, entryable_id, created_at, updated_at ]
40
+ # class Entry < ApplicationRecord
41
+ # belongs_to :account
42
+ # belongs_to :creator
43
+ # delegated_type :entryable, types: %w[ Message Comment ]
44
+ # end
45
+ #
46
+ # module Entryable
47
+ # extend ActiveSupport::Concern
48
+ #
49
+ # included do
50
+ # has_one :entry, as: :entryable, touch: true
51
+ # end
52
+ # end
53
+ #
54
+ # # Schema: messages[ id, subject, body, created_at, updated_at ]
55
+ # class Message < ApplicationRecord
56
+ # include Entryable
57
+ # end
58
+ #
59
+ # # Schema: comments[ id, content, created_at, updated_at ]
60
+ # class Comment < ApplicationRecord
61
+ # include Entryable
62
+ # end
63
+ #
64
+ # As you can see, neither +Message+ nor +Comment+ are meant to stand alone. Crucial metadata for both classes
65
+ # resides in the +Entry+ "superclass". But the +Entry+ absolutely can stand alone in terms of querying capacity
66
+ # in particular. You can now easily do things like:
67
+ #
68
+ # Account.find(1).entries.order(created_at: :desc).limit(50)
69
+ #
70
+ # Which is exactly what you want when displaying both comments and messages together. The entry itself can
71
+ # be rendered as its delegated type easily, like so:
72
+ #
73
+ # # entries/_entry.html.erb
74
+ # <%= render "entries/entryables/#{entry.entryable_name}", entry: entry %>
75
+ #
76
+ # # entries/entryables/_message.html.erb
77
+ # <div class="message">
78
+ # <div class="subject"><%= entry.message.subject %></div>
79
+ # <p><%= entry.message.body %></p>
80
+ # <i>Posted on <%= entry.created_at %> by <%= entry.creator.name %></i>
81
+ # </div>
82
+ #
83
+ # # entries/entryables/_comment.html.erb
84
+ # <div class="comment">
85
+ # <%= entry.creator.name %> said: <%= entry.comment.content %>
86
+ # </div>
87
+ #
88
+ # == Sharing behavior with concerns and controllers
89
+ #
90
+ # The entry "superclass" also serves as a perfect place to put all that shared logic that applies to both
91
+ # messages and comments, and which acts primarily on the shared attributes. Imagine:
92
+ #
93
+ # class Entry < ApplicationRecord
94
+ # include Eventable, Forwardable, Redeliverable
95
+ # end
96
+ #
97
+ # Which allows you to have controllers for things like +ForwardsController+ and +RedeliverableController+
98
+ # that both act on entries, and thus provide the shared functionality to both messages and comments.
99
+ #
100
+ # == Creating new records
101
+ #
102
+ # You create a new record that uses delegated typing by creating the delegator and delegatee at the same time,
103
+ # like so:
104
+ #
105
+ # Entry.create! entryable: Comment.new(content: "Hello!"), creator: Current.user, account: Current.account
106
+ #
107
+ # If you need more complicated composition, or you need to perform dependent validation, you should build a factory
108
+ # method or class to take care of the complicated needs. This could be as simple as:
109
+ #
110
+ # class Entry < ApplicationRecord
111
+ # def self.create_with_comment(content, creator: Current.user, account: Current.account)
112
+ # create! entryable: Comment.new(content: content), creator: creator, account: account
113
+ # end
114
+ # end
115
+ #
116
+ # == Querying across records
117
+ #
118
+ # A consequence of delegated types is that querying attributes spread across multiple classes becomes slightly more
119
+ # tricky, but not impossible.
120
+ #
121
+ # The simplest method is to join the "superclass" to the "subclass" and apply the query parameters (i.e. <tt>#where</tt>)
122
+ # in appropriate places:
123
+ #
124
+ # Comment.joins(:entry).where(comments: { content: 'Hello!' }, entry: { creator: Current.user } )
125
+ #
126
+ # For convenience, add a scope on the concern. Now all classes that implement the concern will automatically include
127
+ # the method:
128
+ #
129
+ # # app/models/concerns/entryable.rb
130
+ # scope :with_entry, ->(attrs) { joins(:entry).where(entry: attrs) }
131
+ #
132
+ # Now the query can be shortened significantly:
133
+ #
134
+ # Comment.where(content: 'Hello!').with_entry(creator: Current.user)
135
+ #
136
+ # == Adding further delegation
137
+ #
138
+ # The delegated type shouldn't just answer the question of what the underlying class is called. In fact, that's
139
+ # an anti-pattern most of the time. The reason you're building this hierarchy is to take advantage of polymorphism.
140
+ # So here's a simple example of that:
141
+ #
142
+ # class Entry < ApplicationRecord
143
+ # delegated_type :entryable, types: %w[ Message Comment ]
144
+ # delegate :title, to: :entryable
145
+ # end
146
+ #
147
+ # class Message < ApplicationRecord
148
+ # def title
149
+ # subject
150
+ # end
151
+ # end
152
+ #
153
+ # class Comment < ApplicationRecord
154
+ # def title
155
+ # content.truncate(20)
156
+ # end
157
+ # end
158
+ #
159
+ # Now you can list a bunch of entries, call <tt>Entry#title</tt>, and polymorphism will provide you with the answer.
160
+ #
161
+ # == Nested \Attributes
162
+ #
163
+ # Enabling nested attributes on a delegated_type association allows you to
164
+ # create the entry and message in one go:
165
+ #
166
+ # class Entry < ApplicationRecord
167
+ # delegated_type :entryable, types: %w[ Message Comment ]
168
+ # accepts_nested_attributes_for :entryable
169
+ # end
170
+ #
171
+ # params = { entry: { entryable_type: 'Message', entryable_attributes: { subject: 'Smiling' } } }
172
+ # entry = Entry.create(params[:entry])
173
+ # entry.entryable.id # => 2
174
+ # entry.entryable.subject # => 'Smiling'
175
+ module DelegatedType
176
+ # Defines this as a class that'll delegate its type for the passed +role+ to the class references in +types+.
177
+ # That'll create a polymorphic +belongs_to+ relationship to that +role+, and it'll add all the delegated
178
+ # type convenience methods:
179
+ #
180
+ # class Entry < ApplicationRecord
181
+ # delegated_type :entryable, types: %w[ Message Comment ], dependent: :destroy
182
+ # end
183
+ #
184
+ # @entry.entryable_class # => Message or Comment
185
+ # @entry.entryable_name # => "message" or "comment"
186
+ # Entry.messages # => Entry.where(entryable_type: "Message")
187
+ # @entry.message? # => true when entryable_type == "Message"
188
+ # @entry.message # => returns the message record, when entryable_type == "Message", otherwise nil
189
+ # @entry.message_id # => returns entryable_id, when entryable_type == "Message", otherwise nil
190
+ # Entry.comments # => Entry.where(entryable_type: "Comment")
191
+ # @entry.comment? # => true when entryable_type == "Comment"
192
+ # @entry.comment # => returns the comment record, when entryable_type == "Comment", otherwise nil
193
+ # @entry.comment_id # => returns entryable_id, when entryable_type == "Comment", otherwise nil
194
+ #
195
+ # You can also declare namespaced types:
196
+ #
197
+ # class Entry < ApplicationRecord
198
+ # delegated_type :entryable, types: %w[ Message Comment Access::NoticeMessage ], dependent: :destroy
199
+ # end
200
+ #
201
+ # Entry.access_notice_messages
202
+ # @entry.access_notice_message
203
+ # @entry.access_notice_message?
204
+ #
205
+ # ==== Options
206
+ #
207
+ # The +options+ are passed directly to the +belongs_to+ call, so this is where you declare +dependent+ etc.
208
+ # The following options can be included to specialize the behavior of the delegated type convenience methods.
209
+ #
210
+ # [+:foreign_key+]
211
+ # Specify the foreign key used for the convenience methods. By default this is guessed to be the passed
212
+ # +role+ with an "_id" suffix. So a class that defines a
213
+ # <tt>delegated_type :entryable, types: %w[ Message Comment ]</tt> association will use "entryable_id" as
214
+ # the default <tt>:foreign_key</tt>.
215
+ # [+:foreign_type+]
216
+ # Specify the column used to store the associated object's type. By default this is inferred to be the passed
217
+ # +role+ with a "_type" suffix. A class that defines a
218
+ # <tt>delegated_type :entryable, types: %w[ Message Comment ]</tt> association will use "entryable_type" as
219
+ # the default <tt>:foreign_type</tt>.
220
+ # [+:primary_key+]
221
+ # Specify the method that returns the primary key of associated object used for the convenience methods.
222
+ # By default this is +id+.
223
+ #
224
+ # Option examples:
225
+ # class Entry < ApplicationRecord
226
+ # delegated_type :entryable, types: %w[ Message Comment ], primary_key: :uuid, foreign_key: :entryable_uuid
227
+ # end
228
+ #
229
+ # @entry.message_uuid # => returns entryable_uuid, when entryable_type == "Message", otherwise nil
230
+ # @entry.comment_uuid # => returns entryable_uuid, when entryable_type == "Comment", otherwise nil
231
+ def delegated_type(role, types:, **options)
232
+ belongs_to role, options.delete(:scope), **options.merge(polymorphic: true)
233
+ define_delegated_type_methods role, types: types, options: options
234
+ end
235
+
236
+ private
237
+ def define_delegated_type_methods(role, types:, options:)
238
+ primary_key = options[:primary_key] || "id"
239
+ role_type = options[:foreign_type] || "#{role}_type"
240
+ role_id = options[:foreign_key] || "#{role}_id"
241
+
242
+ define_singleton_method "#{role}_types" do
243
+ types.map(&:to_s)
244
+ end
245
+
246
+ define_method "#{role}_class" do
247
+ public_send(role_type).constantize
248
+ end
249
+
250
+ define_method "#{role}_name" do
251
+ public_send("#{role}_class").model_name.singular.inquiry
252
+ end
253
+
254
+ define_method "build_#{role}" do |*params|
255
+ public_send("#{role}=", public_send("#{role}_class").new(*params))
256
+ end
257
+
258
+ types.each do |type|
259
+ scope_name = type.tableize.tr("/", "_")
260
+ singular = scope_name.singularize
261
+ query = "#{singular}?"
262
+
263
+ scope scope_name, -> { where(role_type => type) }
264
+
265
+ define_method query do
266
+ public_send(role_type) == type
267
+ end
268
+
269
+ define_method singular do
270
+ public_send(role) if public_send(query)
271
+ end
272
+
273
+ define_method "#{singular}_#{primary_key}" do
274
+ public_send(role_id) if public_send(query)
275
+ end
276
+ end
277
+ end
278
+ end
279
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ def self.deprecator # :nodoc:
5
+ @deprecator ||= ActiveSupport::Deprecation.new
6
+ end
7
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ class DestroyAssociationAsyncError < StandardError
5
+ end
6
+
7
+ # = Active Record Destroy Association Async Job
8
+ #
9
+ # Job to destroy the records associated with a destroyed record in background.
10
+ class DestroyAssociationAsyncJob < ActiveJob::Base
11
+ queue_as { ActiveRecord.queues[:destroy] }
12
+
13
+ discard_on ActiveJob::DeserializationError
14
+
15
+ def perform(
16
+ owner_model_name: nil, owner_id: nil,
17
+ association_class: nil, association_ids: nil, association_primary_key_column: nil,
18
+ ensuring_owner_was_method: nil
19
+ )
20
+ association_model = association_class.constantize
21
+ owner_class = owner_model_name.constantize
22
+ owner = owner_class.find_by(owner_class.primary_key => [owner_id])
23
+
24
+ if !owner_destroyed?(owner, ensuring_owner_was_method)
25
+ raise DestroyAssociationAsyncError, "owner record not destroyed"
26
+ end
27
+
28
+ association_model.where(association_primary_key_column => association_ids).find_each do |r|
29
+ r.destroy
30
+ end
31
+ end
32
+
33
+ private
34
+ def owner_destroyed?(owner, ensuring_owner_was_method)
35
+ !owner || (ensuring_owner_was_method && owner.public_send(ensuring_owner_was_method))
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ class DisableJoinsAssociationRelation < Relation # :nodoc:
5
+ attr_reader :ids, :key
6
+
7
+ def initialize(klass, key, ids)
8
+ @ids = ids.uniq
9
+ @key = key
10
+ super(klass)
11
+ end
12
+
13
+ def limit(value)
14
+ records.take(value)
15
+ end
16
+
17
+ def first(limit = nil)
18
+ if limit
19
+ records.limit(limit).first
20
+ else
21
+ records.first
22
+ end
23
+ end
24
+
25
+ def load
26
+ super
27
+ records = @records
28
+
29
+ records_by_id = records.group_by do |record|
30
+ record[key]
31
+ end
32
+
33
+ records = ids.flat_map { |id| records_by_id[id] }
34
+ records.compact!
35
+
36
+ @records = records
37
+ end
38
+ end
39
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecord
4
- module DynamicMatchers #:nodoc:
4
+ module DynamicMatchers # :nodoc:
5
5
  private
6
6
  def respond_to_missing?(name, _)
7
7
  if self == Base
@@ -12,12 +12,12 @@ module ActiveRecord
12
12
  end
13
13
  end
14
14
 
15
- def method_missing(name, *arguments, &block)
15
+ def method_missing(name, ...)
16
16
  match = Method.match(self, name)
17
17
 
18
18
  if match && match.valid?
19
19
  match.define
20
- send(name, *arguments, &block)
20
+ send(name, ...)
21
21
  else
22
22
  super
23
23
  end
@@ -49,9 +49,9 @@ module ActiveRecord
49
49
 
50
50
  attr_reader :model, :name, :attribute_names
51
51
 
52
- def initialize(model, name)
52
+ def initialize(model, method_name)
53
53
  @model = model
54
- @name = name.to_s
54
+ @name = method_name.to_s
55
55
  @attribute_names = @name.match(self.class.pattern)[1].split("_and_")
56
56
  @attribute_names.map! { |name| @model.attribute_aliases[name] || name }
57
57
  end
@@ -69,7 +69,6 @@ module ActiveRecord
69
69
  end
70
70
 
71
71
  private
72
-
73
72
  def body
74
73
  "#{finder}(#{attributes_hash})"
75
74
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Encryption
5
+ class AutoFilteredParameters
6
+ def initialize(app)
7
+ @app = app
8
+ @attributes_by_class = Concurrent::Map.new
9
+ @collecting = true
10
+
11
+ install_collecting_hook
12
+ end
13
+
14
+ def enable
15
+ apply_collected_attributes
16
+ @collecting = false
17
+ end
18
+
19
+ private
20
+ attr_reader :app
21
+
22
+ def install_collecting_hook
23
+ ActiveRecord::Encryption.on_encrypted_attribute_declared do |klass, attribute|
24
+ attribute_was_declared(klass, attribute)
25
+ end
26
+ end
27
+
28
+ def attribute_was_declared(klass, attribute)
29
+ if collecting?
30
+ collect_for_later(klass, attribute)
31
+ else
32
+ apply_filter(klass, attribute)
33
+ end
34
+ end
35
+
36
+ def apply_collected_attributes
37
+ @attributes_by_class.each do |klass, attributes|
38
+ attributes.each do |attribute|
39
+ apply_filter(klass, attribute)
40
+ end
41
+ end
42
+ end
43
+
44
+ def collecting?
45
+ @collecting
46
+ end
47
+
48
+ def collect_for_later(klass, attribute)
49
+ @attributes_by_class[klass] ||= Concurrent::Array.new
50
+ @attributes_by_class[klass] << attribute
51
+ end
52
+
53
+ def apply_filter(klass, attribute)
54
+ filter = [("#{klass.model_name.element}" if klass.name), attribute.to_s].compact.join(".")
55
+ unless excluded_from_filter_parameters?(filter)
56
+ app.config.filter_parameters << filter unless app.config.filter_parameters.include?(filter)
57
+ klass.filter_attributes += [ attribute ]
58
+ end
59
+ end
60
+
61
+ def excluded_from_filter_parameters?(filter_parameter)
62
+ ActiveRecord::Encryption.config.excluded_from_filter_parameters.find { |excluded_filter| excluded_filter.to_s == filter_parameter }
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module ActiveRecord
6
+ module Encryption
7
+ class Cipher
8
+ # A 256-GCM cipher.
9
+ #
10
+ # By default it will use random initialization vectors. For deterministic encryption, it will use a SHA-256 hash of
11
+ # the text to encrypt and the secret.
12
+ #
13
+ # See +Encryptor+
14
+ class Aes256Gcm
15
+ CIPHER_TYPE = "aes-256-gcm"
16
+
17
+ class << self
18
+ def key_length
19
+ OpenSSL::Cipher.new(CIPHER_TYPE).key_len
20
+ end
21
+
22
+ def iv_length
23
+ OpenSSL::Cipher.new(CIPHER_TYPE).iv_len
24
+ end
25
+ end
26
+
27
+ # When iv not provided, it will generate a random iv on each encryption operation (default and
28
+ # recommended operation)
29
+ def initialize(secret, deterministic: false)
30
+ @secret = secret
31
+ @deterministic = deterministic
32
+ end
33
+
34
+ def encrypt(clear_text)
35
+ # This code is extracted from +ActiveSupport::MessageEncryptor+. Not using it directly because we want to control
36
+ # the message format and only serialize things once at the +ActiveRecord::Encryption::Message+ level. Also, this
37
+ # cipher is prepared to deal with deterministic/non deterministic encryption modes.
38
+
39
+ cipher = OpenSSL::Cipher.new(CIPHER_TYPE)
40
+ cipher.encrypt
41
+ cipher.key = @secret
42
+
43
+ iv = generate_iv(cipher, clear_text)
44
+ cipher.iv = iv
45
+
46
+ encrypted_data = clear_text.empty? ? clear_text.dup : cipher.update(clear_text)
47
+ encrypted_data << cipher.final
48
+
49
+ ActiveRecord::Encryption::Message.new(payload: encrypted_data).tap do |message|
50
+ message.headers.iv = iv
51
+ message.headers.auth_tag = cipher.auth_tag
52
+ end
53
+ end
54
+
55
+ def decrypt(encrypted_message)
56
+ encrypted_data = encrypted_message.payload
57
+ iv = encrypted_message.headers.iv
58
+ auth_tag = encrypted_message.headers.auth_tag
59
+
60
+ # Currently the OpenSSL bindings do not raise an error if auth_tag is
61
+ # truncated, which would allow an attacker to easily forge it. See
62
+ # https://github.com/ruby/openssl/issues/63
63
+ raise ActiveRecord::Encryption::Errors::EncryptedContentIntegrity if auth_tag.nil? || auth_tag.bytes.length != 16
64
+
65
+ cipher = OpenSSL::Cipher.new(CIPHER_TYPE)
66
+
67
+ cipher.decrypt
68
+ cipher.key = @secret
69
+ cipher.iv = iv
70
+
71
+ cipher.auth_tag = auth_tag
72
+ cipher.auth_data = ""
73
+
74
+ decrypted_data = encrypted_data.empty? ? encrypted_data : cipher.update(encrypted_data)
75
+ decrypted_data << cipher.final
76
+
77
+ decrypted_data
78
+ rescue OpenSSL::Cipher::CipherError, TypeError, ArgumentError
79
+ raise ActiveRecord::Encryption::Errors::Decryption
80
+ end
81
+
82
+ def inspect # :nodoc:
83
+ "#<#{self.class.name}:#{'%#016x' % (object_id << 1)}>"
84
+ end
85
+
86
+ private
87
+ def generate_iv(cipher, clear_text)
88
+ if @deterministic
89
+ generate_deterministic_iv(clear_text)
90
+ else
91
+ cipher.random_iv
92
+ end
93
+ end
94
+
95
+ def generate_deterministic_iv(clear_text)
96
+ OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, @secret, clear_text)[0, ActiveRecord::Encryption.cipher.iv_length]
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Encryption
5
+ # The algorithm used for encrypting and decrypting +Message+ objects.
6
+ #
7
+ # It uses AES-256-GCM. It will generate a random IV for non deterministic encryption (default)
8
+ # or derive an initialization vector from the encrypted content for deterministic encryption.
9
+ #
10
+ # See +Cipher::Aes256Gcm+.
11
+ class Cipher
12
+ DEFAULT_ENCODING = Encoding::UTF_8
13
+
14
+ # Encrypts the provided text and return an encrypted +Message+.
15
+ def encrypt(clean_text, key:, deterministic: false)
16
+ cipher_for(key, deterministic: deterministic).encrypt(clean_text).tap do |message|
17
+ message.headers.encoding = clean_text.encoding.name unless clean_text.encoding == DEFAULT_ENCODING
18
+ end
19
+ end
20
+
21
+ # Decrypt the provided +Message+.
22
+ #
23
+ # When +key+ is an Array, it will try all the keys raising a
24
+ # +ActiveRecord::Encryption::Errors::Decryption+ if none works.
25
+ def decrypt(encrypted_message, key:)
26
+ try_to_decrypt_with_each(encrypted_message, keys: Array(key)).tap do |decrypted_text|
27
+ decrypted_text.force_encoding(encrypted_message.headers.encoding || DEFAULT_ENCODING)
28
+ end
29
+ end
30
+
31
+ def key_length
32
+ Aes256Gcm.key_length
33
+ end
34
+
35
+ def iv_length
36
+ Aes256Gcm.iv_length
37
+ end
38
+
39
+ private
40
+ def try_to_decrypt_with_each(encrypted_text, keys:)
41
+ keys.each.with_index do |key, index|
42
+ return cipher_for(key).decrypt(encrypted_text)
43
+ rescue ActiveRecord::Encryption::Errors::Decryption
44
+ raise if index == keys.length - 1
45
+ end
46
+ end
47
+
48
+ def cipher_for(secret, deterministic: false)
49
+ Aes256Gcm.new(secret, deterministic: deterministic)
50
+ end
51
+ end
52
+ end
53
+ end