activerecord 6.0.5 → 6.1.0.rc1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (242) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +764 -888
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +3 -3
  5. data/lib/active_record/aggregations.rb +1 -1
  6. data/lib/active_record/association_relation.rb +22 -14
  7. data/lib/active_record/associations/alias_tracker.rb +19 -15
  8. data/lib/active_record/associations/association.rb +39 -27
  9. data/lib/active_record/associations/association_scope.rb +11 -15
  10. data/lib/active_record/associations/belongs_to_association.rb +15 -5
  11. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +1 -1
  12. data/lib/active_record/associations/builder/association.rb +9 -3
  13. data/lib/active_record/associations/builder/belongs_to.rb +10 -7
  14. data/lib/active_record/associations/builder/collection_association.rb +5 -4
  15. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +0 -1
  16. data/lib/active_record/associations/builder/has_many.rb +6 -2
  17. data/lib/active_record/associations/builder/has_one.rb +11 -14
  18. data/lib/active_record/associations/builder/singular_association.rb +1 -1
  19. data/lib/active_record/associations/collection_association.rb +19 -13
  20. data/lib/active_record/associations/collection_proxy.rb +12 -5
  21. data/lib/active_record/associations/foreign_association.rb +13 -0
  22. data/lib/active_record/associations/has_many_association.rb +24 -2
  23. data/lib/active_record/associations/has_many_through_association.rb +10 -4
  24. data/lib/active_record/associations/has_one_association.rb +15 -1
  25. data/lib/active_record/associations/join_dependency/join_association.rb +29 -14
  26. data/lib/active_record/associations/join_dependency/join_part.rb +1 -1
  27. data/lib/active_record/associations/join_dependency.rb +63 -49
  28. data/lib/active_record/associations/preloader/association.rb +13 -5
  29. data/lib/active_record/associations/preloader/through_association.rb +1 -1
  30. data/lib/active_record/associations/preloader.rb +5 -3
  31. data/lib/active_record/associations/singular_association.rb +1 -1
  32. data/lib/active_record/associations.rb +114 -11
  33. data/lib/active_record/attribute_assignment.rb +10 -8
  34. data/lib/active_record/attribute_methods/before_type_cast.rb +13 -9
  35. data/lib/active_record/attribute_methods/dirty.rb +1 -11
  36. data/lib/active_record/attribute_methods/primary_key.rb +6 -2
  37. data/lib/active_record/attribute_methods/query.rb +3 -6
  38. data/lib/active_record/attribute_methods/read.rb +8 -11
  39. data/lib/active_record/attribute_methods/serialization.rb +4 -4
  40. data/lib/active_record/attribute_methods/time_zone_conversion.rb +12 -13
  41. data/lib/active_record/attribute_methods/write.rb +12 -20
  42. data/lib/active_record/attribute_methods.rb +52 -48
  43. data/lib/active_record/attributes.rb +27 -7
  44. data/lib/active_record/autosave_association.rb +47 -30
  45. data/lib/active_record/base.rb +2 -14
  46. data/lib/active_record/callbacks.rb +32 -22
  47. data/lib/active_record/coders/yaml_column.rb +1 -1
  48. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +180 -134
  49. data/lib/active_record/connection_adapters/abstract/database_limits.rb +2 -44
  50. data/lib/active_record/connection_adapters/abstract/database_statements.rb +65 -22
  51. data/lib/active_record/connection_adapters/abstract/query_cache.rb +2 -7
  52. data/lib/active_record/connection_adapters/abstract/quoting.rb +34 -34
  53. data/lib/active_record/connection_adapters/abstract/savepoints.rb +3 -3
  54. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +153 -116
  55. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +110 -30
  56. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +3 -3
  57. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +224 -85
  58. data/lib/active_record/connection_adapters/abstract/transaction.rb +66 -24
  59. data/lib/active_record/connection_adapters/abstract_adapter.rb +31 -70
  60. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +123 -87
  61. data/lib/active_record/connection_adapters/column.rb +15 -1
  62. data/lib/active_record/connection_adapters/deduplicable.rb +29 -0
  63. data/lib/active_record/connection_adapters/legacy_pool_manager.rb +31 -0
  64. data/lib/active_record/connection_adapters/mysql/database_statements.rb +22 -24
  65. data/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb +1 -1
  66. data/lib/active_record/connection_adapters/mysql/quoting.rb +1 -1
  67. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +33 -6
  68. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +8 -0
  69. data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +1 -1
  70. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +3 -3
  71. data/lib/active_record/connection_adapters/mysql/type_metadata.rb +10 -1
  72. data/lib/active_record/connection_adapters/mysql2_adapter.rb +31 -12
  73. data/lib/active_record/connection_adapters/pool_config.rb +63 -0
  74. data/lib/active_record/connection_adapters/pool_manager.rb +43 -0
  75. data/lib/active_record/connection_adapters/postgresql/column.rb +24 -1
  76. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +12 -53
  77. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +3 -5
  78. data/lib/active_record/connection_adapters/postgresql/oid/date.rb +2 -2
  79. data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +2 -10
  80. data/lib/active_record/connection_adapters/postgresql/oid/interval.rb +49 -0
  81. data/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb +2 -2
  82. data/lib/active_record/connection_adapters/postgresql/oid/macaddr.rb +25 -0
  83. data/lib/active_record/connection_adapters/postgresql/oid/money.rb +2 -2
  84. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +2 -2
  85. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +11 -1
  86. data/lib/active_record/connection_adapters/postgresql/oid.rb +2 -0
  87. data/lib/active_record/connection_adapters/postgresql/quoting.rb +4 -4
  88. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +1 -1
  89. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +5 -1
  90. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +61 -29
  91. data/lib/active_record/connection_adapters/postgresql/type_metadata.rb +8 -0
  92. data/lib/active_record/connection_adapters/postgresql_adapter.rb +72 -55
  93. data/lib/active_record/connection_adapters/schema_cache.rb +98 -15
  94. data/lib/active_record/connection_adapters/sql_type_metadata.rb +10 -0
  95. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +30 -5
  96. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +1 -1
  97. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +5 -1
  98. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +36 -3
  99. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +48 -50
  100. data/lib/active_record/connection_adapters.rb +50 -0
  101. data/lib/active_record/connection_handling.rb +210 -71
  102. data/lib/active_record/core.rb +215 -49
  103. data/lib/active_record/database_configurations/connection_url_resolver.rb +98 -0
  104. data/lib/active_record/database_configurations/database_config.rb +52 -9
  105. data/lib/active_record/database_configurations/hash_config.rb +54 -8
  106. data/lib/active_record/database_configurations/url_config.rb +15 -40
  107. data/lib/active_record/database_configurations.rb +124 -85
  108. data/lib/active_record/delegated_type.rb +209 -0
  109. data/lib/active_record/destroy_association_async_job.rb +36 -0
  110. data/lib/active_record/enum.rb +33 -23
  111. data/lib/active_record/errors.rb +47 -12
  112. data/lib/active_record/explain.rb +9 -4
  113. data/lib/active_record/explain_subscriber.rb +1 -1
  114. data/lib/active_record/fixture_set/file.rb +10 -17
  115. data/lib/active_record/fixture_set/model_metadata.rb +1 -2
  116. data/lib/active_record/fixture_set/render_context.rb +1 -1
  117. data/lib/active_record/fixture_set/table_row.rb +2 -2
  118. data/lib/active_record/fixtures.rb +54 -8
  119. data/lib/active_record/gem_version.rb +3 -3
  120. data/lib/active_record/inheritance.rb +40 -18
  121. data/lib/active_record/insert_all.rb +32 -5
  122. data/lib/active_record/integration.rb +3 -5
  123. data/lib/active_record/internal_metadata.rb +15 -4
  124. data/lib/active_record/legacy_yaml_adapter.rb +7 -3
  125. data/lib/active_record/locking/optimistic.rb +13 -16
  126. data/lib/active_record/locking/pessimistic.rb +6 -2
  127. data/lib/active_record/log_subscriber.rb +26 -8
  128. data/lib/active_record/middleware/database_selector/resolver/session.rb +3 -0
  129. data/lib/active_record/middleware/database_selector/resolver.rb +5 -0
  130. data/lib/active_record/middleware/database_selector.rb +4 -1
  131. data/lib/active_record/migration/command_recorder.rb +47 -27
  132. data/lib/active_record/migration/compatibility.rb +67 -17
  133. data/lib/active_record/migration.rb +113 -83
  134. data/lib/active_record/model_schema.rb +88 -42
  135. data/lib/active_record/nested_attributes.rb +2 -3
  136. data/lib/active_record/no_touching.rb +1 -1
  137. data/lib/active_record/persistence.rb +50 -45
  138. data/lib/active_record/query_cache.rb +15 -5
  139. data/lib/active_record/querying.rb +11 -6
  140. data/lib/active_record/railtie.rb +64 -44
  141. data/lib/active_record/railties/databases.rake +253 -98
  142. data/lib/active_record/readonly_attributes.rb +4 -0
  143. data/lib/active_record/reflection.rb +59 -44
  144. data/lib/active_record/relation/batches/batch_enumerator.rb +25 -9
  145. data/lib/active_record/relation/batches.rb +38 -31
  146. data/lib/active_record/relation/calculations.rb +100 -43
  147. data/lib/active_record/relation/finder_methods.rb +44 -14
  148. data/lib/active_record/relation/from_clause.rb +1 -1
  149. data/lib/active_record/relation/merger.rb +20 -23
  150. data/lib/active_record/relation/predicate_builder/array_handler.rb +8 -9
  151. data/lib/active_record/relation/predicate_builder/association_query_value.rb +2 -2
  152. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +3 -3
  153. data/lib/active_record/relation/predicate_builder/relation_handler.rb +1 -1
  154. data/lib/active_record/relation/predicate_builder.rb +57 -33
  155. data/lib/active_record/relation/query_methods.rb +319 -196
  156. data/lib/active_record/relation/record_fetch_warning.rb +3 -3
  157. data/lib/active_record/relation/spawn_methods.rb +6 -5
  158. data/lib/active_record/relation/where_clause.rb +104 -57
  159. data/lib/active_record/relation.rb +90 -64
  160. data/lib/active_record/result.rb +41 -33
  161. data/lib/active_record/runtime_registry.rb +2 -2
  162. data/lib/active_record/sanitization.rb +6 -17
  163. data/lib/active_record/schema_dumper.rb +34 -4
  164. data/lib/active_record/schema_migration.rb +0 -4
  165. data/lib/active_record/scoping/named.rb +1 -17
  166. data/lib/active_record/secure_token.rb +16 -8
  167. data/lib/active_record/serialization.rb +5 -3
  168. data/lib/active_record/signed_id.rb +116 -0
  169. data/lib/active_record/statement_cache.rb +20 -4
  170. data/lib/active_record/store.rb +2 -2
  171. data/lib/active_record/suppressor.rb +2 -2
  172. data/lib/active_record/table_metadata.rb +36 -52
  173. data/lib/active_record/tasks/database_tasks.rb +139 -113
  174. data/lib/active_record/tasks/mysql_database_tasks.rb +34 -35
  175. data/lib/active_record/tasks/postgresql_database_tasks.rb +24 -26
  176. data/lib/active_record/tasks/sqlite_database_tasks.rb +13 -9
  177. data/lib/active_record/test_databases.rb +5 -4
  178. data/lib/active_record/test_fixtures.rb +36 -33
  179. data/lib/active_record/timestamp.rb +4 -6
  180. data/lib/active_record/touch_later.rb +21 -21
  181. data/lib/active_record/transactions.rb +15 -64
  182. data/lib/active_record/type/serialized.rb +6 -2
  183. data/lib/active_record/type.rb +8 -1
  184. data/lib/active_record/type_caster/connection.rb +0 -1
  185. data/lib/active_record/type_caster/map.rb +8 -5
  186. data/lib/active_record/validations/associated.rb +1 -1
  187. data/lib/active_record/validations/numericality.rb +35 -0
  188. data/lib/active_record/validations/uniqueness.rb +24 -4
  189. data/lib/active_record/validations.rb +1 -0
  190. data/lib/active_record.rb +7 -14
  191. data/lib/arel/attributes/attribute.rb +4 -0
  192. data/lib/arel/collectors/bind.rb +5 -0
  193. data/lib/arel/collectors/composite.rb +8 -0
  194. data/lib/arel/collectors/sql_string.rb +7 -0
  195. data/lib/arel/collectors/substitute_binds.rb +7 -0
  196. data/lib/arel/nodes/binary.rb +82 -8
  197. data/lib/arel/nodes/bind_param.rb +8 -0
  198. data/lib/arel/nodes/casted.rb +21 -9
  199. data/lib/arel/nodes/equality.rb +6 -9
  200. data/lib/arel/nodes/grouping.rb +3 -0
  201. data/lib/arel/nodes/homogeneous_in.rb +72 -0
  202. data/lib/arel/nodes/in.rb +8 -1
  203. data/lib/arel/nodes/infix_operation.rb +13 -1
  204. data/lib/arel/nodes/join_source.rb +1 -1
  205. data/lib/arel/nodes/node.rb +7 -6
  206. data/lib/arel/nodes/ordering.rb +27 -0
  207. data/lib/arel/nodes/sql_literal.rb +3 -0
  208. data/lib/arel/nodes/table_alias.rb +7 -3
  209. data/lib/arel/nodes/unary.rb +0 -1
  210. data/lib/arel/nodes.rb +3 -1
  211. data/lib/arel/predications.rb +12 -18
  212. data/lib/arel/select_manager.rb +1 -2
  213. data/lib/arel/table.rb +13 -5
  214. data/lib/arel/visitors/dot.rb +14 -2
  215. data/lib/arel/visitors/mysql.rb +11 -1
  216. data/lib/arel/visitors/postgresql.rb +15 -4
  217. data/lib/arel/visitors/to_sql.rb +89 -78
  218. data/lib/arel/visitors.rb +0 -7
  219. data/lib/arel.rb +5 -13
  220. data/lib/rails/generators/active_record/migration/migration_generator.rb +1 -0
  221. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +2 -0
  222. data/lib/rails/generators/active_record/migration/templates/migration.rb.tt +3 -3
  223. data/lib/rails/generators/active_record/migration.rb +6 -1
  224. data/lib/rails/generators/active_record/model/model_generator.rb +39 -2
  225. data/lib/rails/generators/active_record/model/templates/abstract_base_class.rb.tt +7 -0
  226. metadata +30 -32
  227. data/lib/active_record/advisory_lock_base.rb +0 -18
  228. data/lib/active_record/attribute_decorators.rb +0 -88
  229. data/lib/active_record/connection_adapters/connection_specification.rb +0 -296
  230. data/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb +0 -29
  231. data/lib/active_record/define_callbacks.rb +0 -22
  232. data/lib/active_record/railties/collection_cache_association_loading.rb +0 -34
  233. data/lib/active_record/relation/predicate_builder/base_handler.rb +0 -18
  234. data/lib/active_record/relation/where_clause_factory.rb +0 -33
  235. data/lib/arel/attributes.rb +0 -22
  236. data/lib/arel/visitors/depth_first.rb +0 -203
  237. data/lib/arel/visitors/ibm_db.rb +0 -34
  238. data/lib/arel/visitors/informix.rb +0 -62
  239. data/lib/arel/visitors/mssql.rb +0 -156
  240. data/lib/arel/visitors/oracle.rb +0 -158
  241. data/lib/arel/visitors/oracle12.rb +0 -65
  242. data/lib/arel/visitors/where_sql.rb +0 -22
@@ -13,14 +13,14 @@ module ActiveRecord
13
13
  # Becomes:
14
14
  #
15
15
  # #<ActiveRecord::DatabaseConfigurations::UrlConfig:0x00007fdc3238f340
16
- # @env_name="default_env", @spec_name="primary",
17
- # @config={"adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost"},
16
+ # @env_name="default_env", @name="primary",
17
+ # @config={adapter: "postgresql", database: "foo", host: "localhost"},
18
18
  # @url="postgres://localhost/foo">
19
19
  #
20
20
  # ==== Options
21
21
  #
22
22
  # * <tt>:env_name</tt> - The Rails environment, ie "development".
23
- # * <tt>:spec_name</tt> - The specification name. In a standard two-tier
23
+ # * <tt>:name</tt> - The db config name. In a standard two-tier
24
24
  # database configuration this will default to "primary". In a multiple
25
25
  # database three-tier database configuration this corresponds to the name
26
26
  # used in the second tier, for example "primary_readonly".
@@ -28,49 +28,24 @@ module ActiveRecord
28
28
  # * <tt>:config</tt> - The config hash. This is the hash that contains the
29
29
  # database adapter, name, and other important information for database
30
30
  # connections.
31
- class UrlConfig < DatabaseConfig
32
- attr_reader :url, :config
31
+ class UrlConfig < HashConfig
32
+ attr_reader :url
33
33
 
34
- def initialize(env_name, spec_name, url, config = {})
35
- super(env_name, spec_name)
36
- @config = build_config(config, url)
37
- @url = url
38
- end
39
-
40
- def url_config? # :nodoc:
41
- true
42
- end
43
-
44
- # Determines whether a database configuration is for a replica / readonly
45
- # connection. If the +replica+ key is present in the config, +replica?+ will
46
- # return +true+.
47
- def replica?
48
- config["replica"]
49
- end
34
+ def initialize(env_name, name, url, configuration_hash = {})
35
+ super(env_name, name, configuration_hash)
50
36
 
51
- # The migrations paths for a database configuration. If the
52
- # +migrations_paths+ key is present in the config, +migrations_paths+
53
- # will return its value.
54
- def migrations_paths
55
- config["migrations_paths"]
37
+ @url = url
38
+ @configuration_hash = @configuration_hash.merge(build_url_hash).freeze
56
39
  end
57
40
 
58
41
  private
59
- def build_url_hash(url)
60
- if url.nil? || /^jdbc:/.match?(url)
61
- { "url" => url }
62
- else
63
- ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(url).to_hash
64
- end
65
- end
66
-
67
- def build_config(original_config, url)
68
- hash = build_url_hash(url)
69
-
70
- if original_config[env_name]
71
- original_config[env_name].merge(hash)
42
+ # Return a Hash that can be merged into the main config that represents
43
+ # the passed in url
44
+ def build_url_hash
45
+ if url.nil? || url.start_with?("jdbc:")
46
+ { url: url }
72
47
  else
73
- original_config.merge(hash)
48
+ ConnectionUrlResolver.new(url).to_hash
74
49
  end
75
50
  end
76
51
  end
@@ -3,6 +3,7 @@
3
3
  require "active_record/database_configurations/database_config"
4
4
  require "active_record/database_configurations/hash_config"
5
5
  require "active_record/database_configurations/url_config"
6
+ require "active_record/database_configurations/connection_url_resolver"
6
7
 
7
8
  module ActiveRecord
8
9
  # ActiveRecord::DatabaseConfigurations returns an array of DatabaseConfig
@@ -21,7 +22,7 @@ module ActiveRecord
21
22
  # Collects the configs for the environment and optionally the specification
22
23
  # name passed in. To include replica configurations pass <tt>include_replicas: true</tt>.
23
24
  #
24
- # If a spec name is provided a single DatabaseConfig object will be
25
+ # If a name is provided a single DatabaseConfig object will be
25
26
  # returned, otherwise an array of DatabaseConfig objects will be
26
27
  # returned that corresponds with the environment and type requested.
27
28
  #
@@ -29,13 +30,20 @@ module ActiveRecord
29
30
  #
30
31
  # * <tt>env_name:</tt> The environment name. Defaults to +nil+ which will collect
31
32
  # configs for all environments.
32
- # * <tt>spec_name:</tt> The specification name (i.e. primary, animals, etc.). Defaults
33
- # to +nil+.
33
+ # * <tt>name:</tt> The db config name (i.e. primary, animals, etc.). Defaults
34
+ # to +nil+. If no +env_name+ is specified the config for the default env and the
35
+ # passed +name+ will be returned.
34
36
  # * <tt>include_replicas:</tt> Determines whether to include replicas in
35
37
  # the returned list. Most of the time we're only iterating over the write
36
38
  # connection (i.e. migrations don't need to run for the write and read connection).
37
39
  # Defaults to +false+.
38
- def configs_for(env_name: nil, spec_name: nil, include_replicas: false)
40
+ def configs_for(env_name: nil, spec_name: nil, name: nil, include_replicas: false)
41
+ if spec_name
42
+ name = spec_name
43
+ ActiveSupport::Deprecation.warn("The kwarg `spec_name` is deprecated in favor of `name`. `spec_name` will be removed in Rails 6.2")
44
+ end
45
+
46
+ env_name ||= default_env if name
39
47
  configs = env_with_configs(env_name)
40
48
 
41
49
  unless include_replicas
@@ -44,9 +52,9 @@ module ActiveRecord
44
52
  end
45
53
  end
46
54
 
47
- if spec_name
55
+ if name
48
56
  configs.find do |db_config|
49
- db_config.spec_name == spec_name
57
+ db_config.name == name
50
58
  end
51
59
  else
52
60
  configs
@@ -59,31 +67,46 @@ module ActiveRecord
59
67
  # return the first config hash for the environment.
60
68
  #
61
69
  # { database: "my_db", adapter: "mysql2" }
62
- def default_hash(env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s)
70
+ def default_hash(env = default_env)
63
71
  default = find_db_config(env)
64
- default.config if default
72
+ default.configuration_hash if default
65
73
  end
66
74
  alias :[] :default_hash
75
+ deprecate "[]": "Use configs_for", default_hash: "Use configs_for"
67
76
 
68
77
  # Returns a single DatabaseConfig object based on the requested environment.
69
78
  #
70
79
  # If the application has multiple databases +find_db_config+ will return
71
80
  # the first DatabaseConfig for the environment.
72
81
  def find_db_config(env)
73
- configurations.find do |db_config|
74
- db_config.env_name == env.to_s ||
75
- (db_config.for_current_env? && db_config.spec_name == env.to_s)
76
- end
82
+ configurations
83
+ .sort_by.with_index { |db_config, i| db_config.for_current_env? ? [0, i] : [1, i] }
84
+ .find do |db_config|
85
+ db_config.env_name == env.to_s ||
86
+ (db_config.for_current_env? && db_config.name == env.to_s)
87
+ end
88
+ end
89
+
90
+ # A primary configuration is one that is named primary or if there is
91
+ # no primary, the first configuration for an environment will be treated
92
+ # as primary. This is used as the "default" configuration and is used
93
+ # when the application needs to treat one configuration differently. For
94
+ # example, when Rails dumps the schema, the primary configuration's schema
95
+ # file will be named `schema.rb` instead of `primary_schema.rb`.
96
+ def primary?(name) # :nodoc:
97
+ return true if name == "primary"
98
+
99
+ first_config = find_db_config(default_env)
100
+ first_config && name == first_config.name
77
101
  end
78
102
 
79
103
  # Returns the DatabaseConfigurations object as a Hash.
80
104
  def to_h
81
- configs = configurations.reverse.inject({}) do |memo, db_config|
82
- memo.merge(db_config.to_legacy_hash)
105
+ configurations.inject({}) do |memo, db_config|
106
+ memo.merge(db_config.env_name => db_config.configuration_hash.stringify_keys)
83
107
  end
84
-
85
- Hash[configs.to_a.reverse]
86
108
  end
109
+ deprecate to_h: "You can use `ActiveRecord::Base.configurations.configs_for(env_name: 'env', name: 'primary').configuration_hash` to get the configuration hashes."
87
110
 
88
111
  # Checks if the application's configurations are empty.
89
112
  #
@@ -93,20 +116,43 @@ module ActiveRecord
93
116
  end
94
117
  alias :blank? :empty?
95
118
 
96
- def each
97
- throw_getter_deprecation(:each)
98
- configurations.each { |config|
99
- yield [config.env_name, config.config]
100
- }
101
- end
102
-
103
- def first
104
- throw_getter_deprecation(:first)
105
- config = configurations.first
106
- [config.env_name, config.config]
119
+ # Returns fully resolved connection, accepts hash, string or symbol.
120
+ # Always returns a DatabaseConfiguration::DatabaseConfig
121
+ #
122
+ # == Examples
123
+ #
124
+ # Symbol representing current environment.
125
+ #
126
+ # DatabaseConfigurations.new("production" => {}).resolve(:production)
127
+ # # => DatabaseConfigurations::HashConfig.new(env_name: "production", config: {})
128
+ #
129
+ # One layer deep hash of connection values.
130
+ #
131
+ # DatabaseConfigurations.new({}).resolve("adapter" => "sqlite3")
132
+ # # => DatabaseConfigurations::HashConfig.new(config: {"adapter" => "sqlite3"})
133
+ #
134
+ # Connection URL.
135
+ #
136
+ # DatabaseConfigurations.new({}).resolve("postgresql://localhost/foo")
137
+ # # => DatabaseConfigurations::UrlConfig.new(config: {"adapter" => "postgresql", "host" => "localhost", "database" => "foo"})
138
+ def resolve(config) # :nodoc:
139
+ return config if DatabaseConfigurations::DatabaseConfig === config
140
+
141
+ case config
142
+ when Symbol
143
+ resolve_symbol_connection(config)
144
+ when Hash, String
145
+ build_db_config_from_raw_config(default_env, "primary", config)
146
+ else
147
+ raise TypeError, "Invalid type for configuration. Expected Symbol, String, or Hash. Got #{config.inspect}"
148
+ end
107
149
  end
108
150
 
109
151
  private
152
+ def default_env
153
+ ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s
154
+ end
155
+
110
156
  def env_with_configs(env = nil)
111
157
  if env
112
158
  configurations.select { |db_config| db_config.env_name == env }
@@ -127,107 +173,100 @@ module ActiveRecord
127
173
  end
128
174
  end
129
175
 
130
- current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s
131
-
132
176
  unless db_configs.find(&:for_current_env?)
133
- db_configs << environment_url_config(current_env, "primary", {})
177
+ db_configs << environment_url_config(default_env, "primary", {})
134
178
  end
135
179
 
136
- merge_db_environment_variables(current_env, db_configs.compact)
180
+ merge_db_environment_variables(default_env, db_configs.compact)
137
181
  end
138
182
 
139
183
  def walk_configs(env_name, config)
140
- config.map do |spec_name, sub_config|
141
- build_db_config_from_raw_config(env_name, spec_name.to_s, sub_config)
184
+ config.map do |name, sub_config|
185
+ build_db_config_from_raw_config(env_name, name.to_s, sub_config)
142
186
  end
143
187
  end
144
188
 
145
- def build_db_config_from_raw_config(env_name, spec_name, config)
189
+ def resolve_symbol_connection(name)
190
+ if db_config = find_db_config(name)
191
+ db_config
192
+ else
193
+ raise AdapterNotSpecified, <<~MSG
194
+ The `#{name}` database is not configured for the `#{default_env}` environment.
195
+
196
+ Available databases configurations are:
197
+
198
+ #{build_configuration_sentence}
199
+ MSG
200
+ end
201
+ end
202
+
203
+ def build_configuration_sentence
204
+ configs = configs_for(include_replicas: true)
205
+
206
+ configs.group_by(&:env_name).map do |env, config|
207
+ names = config.map(&:name)
208
+ if names.size > 1
209
+ "#{env}: #{names.join(", ")}"
210
+ else
211
+ env
212
+ end
213
+ end.join("\n")
214
+ end
215
+
216
+ def build_db_config_from_raw_config(env_name, name, config)
146
217
  case config
147
218
  when String
148
- build_db_config_from_string(env_name, spec_name, config)
219
+ build_db_config_from_string(env_name, name, config)
149
220
  when Hash
150
- build_db_config_from_hash(env_name, spec_name, config.stringify_keys)
221
+ build_db_config_from_hash(env_name, name, config.symbolize_keys)
151
222
  else
152
223
  raise InvalidConfigurationError, "'{ #{env_name} => #{config} }' is not a valid configuration. Expected '#{config}' to be a URL string or a Hash."
153
224
  end
154
225
  end
155
226
 
156
- def build_db_config_from_string(env_name, spec_name, config)
227
+ def build_db_config_from_string(env_name, name, config)
157
228
  url = config
158
229
  uri = URI.parse(url)
159
230
  if uri.scheme
160
- ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, spec_name, url)
231
+ UrlConfig.new(env_name, name, url)
161
232
  else
162
233
  raise InvalidConfigurationError, "'{ #{env_name} => #{config} }' is not a valid configuration. Expected '#{config}' to be a URL string or a Hash."
163
234
  end
164
235
  end
165
236
 
166
- def build_db_config_from_hash(env_name, spec_name, config)
167
- if config.has_key?("url")
168
- url = config["url"]
237
+ def build_db_config_from_hash(env_name, name, config)
238
+ if config.has_key?(:url)
239
+ url = config[:url]
169
240
  config_without_url = config.dup
170
- config_without_url.delete "url"
241
+ config_without_url.delete :url
171
242
 
172
- ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, spec_name, url, config_without_url)
243
+ UrlConfig.new(env_name, name, url, config_without_url)
173
244
  else
174
- ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config)
245
+ HashConfig.new(env_name, name, config)
175
246
  end
176
247
  end
177
248
 
178
249
  def merge_db_environment_variables(current_env, configs)
179
250
  configs.map do |config|
180
- next config if config.url_config? || config.env_name != current_env
251
+ next config if config.is_a?(UrlConfig) || config.env_name != current_env
181
252
 
182
- url_config = environment_url_config(current_env, config.spec_name, config.config)
253
+ url_config = environment_url_config(current_env, config.name, config.configuration_hash)
183
254
  url_config || config
184
255
  end
185
256
  end
186
257
 
187
- def environment_url_config(env, spec_name, config)
188
- url = environment_value_for(spec_name)
258
+ def environment_url_config(env, name, config)
259
+ url = environment_value_for(name)
189
260
  return unless url
190
261
 
191
- ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, spec_name, url, config)
262
+ UrlConfig.new(env, name, url, config)
192
263
  end
193
264
 
194
- def environment_value_for(spec_name)
195
- spec_env_key = "#{spec_name.upcase}_DATABASE_URL"
196
- url = ENV[spec_env_key]
197
- url ||= ENV["DATABASE_URL"] if spec_name == "primary"
265
+ def environment_value_for(name)
266
+ name_env_key = "#{name.upcase}_DATABASE_URL"
267
+ url = ENV[name_env_key]
268
+ url ||= ENV["DATABASE_URL"] if name == "primary"
198
269
  url
199
270
  end
200
-
201
- def method_missing(method, *args, &blk)
202
- case method
203
- when :fetch
204
- throw_getter_deprecation(method)
205
- configs_for(env_name: args.first)
206
- when :values
207
- throw_getter_deprecation(method)
208
- configurations.map(&:config)
209
- when :[]=
210
- throw_setter_deprecation(method)
211
-
212
- env_name = args[0]
213
- config = args[1]
214
-
215
- remaining_configs = configurations.reject { |db_config| db_config.env_name == env_name }
216
- new_config = build_configs(env_name => config)
217
- new_configs = remaining_configs + new_config
218
-
219
- ActiveRecord::Base.configurations = new_configs
220
- else
221
- raise NotImplementedError, "`ActiveRecord::Base.configurations` in Rails 6 now returns an object instead of a hash. The `#{method}` method is not supported. Please use `configs_for` or consult the documentation for supported methods."
222
- end
223
- end
224
-
225
- def throw_setter_deprecation(method)
226
- ActiveSupport::Deprecation.warn("Setting `ActiveRecord::Base.configurations` with `#{method}` is deprecated. Use `ActiveRecord::Base.configurations=` directly to set the configurations instead.")
227
- end
228
-
229
- def throw_getter_deprecation(method)
230
- ActiveSupport::Deprecation.warn("`ActiveRecord::Base.configurations` no longer returns a hash. Methods that act on the hash like `#{method}` are deprecated and will be removed in Rails 6.1. Use the `configs_for` method to collect and iterate over the database configurations.")
231
- end
232
271
  end
233
272
  end
@@ -0,0 +1,209 @@
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, created_at, updated_at, entryable_type, entryable_id ]
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 ]
55
+ # class Message < ApplicationRecord
56
+ # include Entryable
57
+ # has_rich_text :content
58
+ # end
59
+ #
60
+ # # Schema: comments[ id, content ]
61
+ # class Comment < ApplicationRecord
62
+ # include Entryable
63
+ # end
64
+ #
65
+ # As you can see, neither +Message+ nor +Comment+ are meant to stand alone. Crucial metadata for both classes
66
+ # resides in the +Entry+ "superclass". But the +Entry+ absolutely can stand alone in terms of querying capacity
67
+ # in particular. You can now easily do things like:
68
+ #
69
+ # Account.entries.order(created_at: :desc).limit(50)
70
+ #
71
+ # Which is exactly what you want when displaying both comments and messages together. The entry itself can
72
+ # be rendered as its delegated type easily, like so:
73
+ #
74
+ # # entries/_entry.html.erb
75
+ # <%= render "entries/entryables/#{entry.entryable_name}", entry: entry %>
76
+ #
77
+ # # entries/entryables/_message.html.erb
78
+ # <div class="message">
79
+ # Posted on <%= entry.created_at %> by <%= entry.creator.name %>: <%= entry.message.content %>
80
+ # </div>
81
+ #
82
+ # # entries/entryables/_comment.html.erb
83
+ # <div class="comment">
84
+ # <%= entry.creator.name %> said: <%= entry.comment.content %>
85
+ # </div>
86
+ #
87
+ # == Sharing behavior with concerns and controllers
88
+ #
89
+ # The entry "superclass" also serves as a perfect place to put all that shared logic that applies to both
90
+ # messages and comments, and which acts primarily on the shared attributes. Imagine:
91
+ #
92
+ # class Entry < ApplicationRecord
93
+ # include Eventable, Forwardable, Redeliverable
94
+ # end
95
+ #
96
+ # Which allows you to have controllers for things like +ForwardsController+ and +RedeliverableController+
97
+ # that both act on entries, and thus provide the shared functionality to both messages and comments.
98
+ #
99
+ # == Creating new records
100
+ #
101
+ # You create a new record that uses delegated typing by creating the delegator and delegatee at the same time,
102
+ # like so:
103
+ #
104
+ # Entry.create! entryable: Comment.new(content: "Hello!"), creator: Current.user
105
+ #
106
+ # If you need more complicated composition, or you need to perform dependent validation, you should build a factory
107
+ # method or class to take care of the complicated needs. This could be as simple as:
108
+ #
109
+ # class Entry < ApplicationRecord
110
+ # def self.create_with_comment(content, creator: Current.user)
111
+ # create! entryable: Comment.new(content: content), creator: creator
112
+ # end
113
+ # end
114
+ #
115
+ # == Adding further delegation
116
+ #
117
+ # The delegated type shouldn't just answer the question of what the underlying class is called. In fact, that's
118
+ # an anti-pattern most of the time. The reason you're building this hierarchy is to take advantage of polymorphism.
119
+ # So here's a simple example of that:
120
+ #
121
+ # class Entry < ApplicationRecord
122
+ # delegated_type :entryable, types: %w[ Message Comment ]
123
+ # delegate :title, to: :entryable
124
+ # end
125
+ #
126
+ # class Message < ApplicationRecord
127
+ # def title
128
+ # subject
129
+ # end
130
+ # end
131
+ #
132
+ # class Comment < ApplicationRecord
133
+ # def title
134
+ # content.truncate(20)
135
+ # end
136
+ # end
137
+ #
138
+ # Now you can list a bunch of entries, call +Entry#title+, and polymorphism will provide you with the answer.
139
+ module DelegatedType
140
+ # Defines this as a class that'll delegate its type for the passed +role+ to the class references in +types+.
141
+ # That'll create a polymorphic +belongs_to+ relationship to that +role+, and it'll add all the delegated
142
+ # type convenience methods:
143
+ #
144
+ # class Entry < ApplicationRecord
145
+ # delegated_type :entryable, types: %w[ Message Comment ], dependent: :destroy
146
+ # end
147
+ #
148
+ # Entry#entryable_class # => +Message+ or +Comment+
149
+ # Entry#entryable_name # => "message" or "comment"
150
+ # Entry.messages # => Entry.where(entryable_type: "Message")
151
+ # Entry#message? # => true when entryable_type == "Message"
152
+ # Entry#message # => returns the message record, when entryable_type == "Message", otherwise nil
153
+ # Entry#message_id # => returns entryable_id, when entryable_type == "Message", otherwise nil
154
+ # Entry.comments # => Entry.where(entryable_type: "Comment")
155
+ # Entry#comment? # => true when entryable_type == "Comment"
156
+ # Entry#comment # => returns the comment record, when entryable_type == "Comment", otherwise nil
157
+ # Entry#comment_id # => returns entryable_id, when entryable_type == "Comment", otherwise nil
158
+ #
159
+ # The +options+ are passed directly to the +belongs_to+ call, so this is where you declare +dependent+ etc.
160
+ #
161
+ # You can also declare namespaced types:
162
+ #
163
+ # class Entry < ApplicationRecord
164
+ # delegated_type :entryable, types: %w[ Message Comment Access::NoticeMessage ], dependent: :destroy
165
+ # end
166
+ #
167
+ # Entry.access_notice_messages
168
+ # entry.access_notice_message
169
+ # entry.access_notice_message?
170
+ def delegated_type(role, types:, **options)
171
+ belongs_to role, options.delete(:scope), **options.merge(polymorphic: true)
172
+ define_delegated_type_methods role, types: types
173
+ end
174
+
175
+ private
176
+ def define_delegated_type_methods(role, types:)
177
+ role_type = "#{role}_type"
178
+ role_id = "#{role}_id"
179
+
180
+ define_method "#{role}_class" do
181
+ public_send("#{role}_type").constantize
182
+ end
183
+
184
+ define_method "#{role}_name" do
185
+ public_send("#{role}_class").model_name.singular.inquiry
186
+ end
187
+
188
+ types.each do |type|
189
+ scope_name = type.tableize.gsub("/", "_")
190
+ singular = scope_name.singularize
191
+ query = "#{singular}?"
192
+
193
+ scope scope_name, -> { where(role_type => type) }
194
+
195
+ define_method query do
196
+ public_send(role_type) == type
197
+ end
198
+
199
+ define_method singular do
200
+ public_send(role) if public_send(query)
201
+ end
202
+
203
+ define_method "#{singular}_id" do
204
+ public_send(role_id) if public_send(query)
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ class DestroyAssociationAsyncError < StandardError
5
+ end
6
+
7
+ # Job to destroy the records associated with a destroyed record in background.
8
+ class DestroyAssociationAsyncJob < ActiveJob::Base
9
+ queue_as { ActiveRecord::Base.queues[:destroy] }
10
+
11
+ discard_on ActiveJob::DeserializationError
12
+
13
+ def perform(
14
+ owner_model_name: nil, owner_id: nil,
15
+ association_class: nil, association_ids: nil, association_primary_key_column: nil,
16
+ ensuring_owner_was_method: nil
17
+ )
18
+ association_model = association_class.constantize
19
+ owner_class = owner_model_name.constantize
20
+ owner = owner_class.find_by(owner_class.primary_key.to_sym => owner_id)
21
+
22
+ if !owner_destroyed?(owner, ensuring_owner_was_method)
23
+ raise DestroyAssociationAsyncError, "owner record not destroyed"
24
+ end
25
+
26
+ association_model.where(association_primary_key_column => association_ids).find_each do |r|
27
+ r.destroy
28
+ end
29
+ end
30
+
31
+ private
32
+ def owner_destroyed?(owner, ensuring_owner_was_method)
33
+ !owner || (ensuring_owner_was_method && owner.public_send(ensuring_owner_was_method))
34
+ end
35
+ end
36
+ end