activerecord 6.0.0.beta1 → 6.0.1.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 (158) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +529 -10
  3. data/README.rdoc +3 -1
  4. data/lib/active_record.rb +7 -1
  5. data/lib/active_record/association_relation.rb +15 -6
  6. data/lib/active_record/associations.rb +4 -3
  7. data/lib/active_record/associations/association.rb +27 -2
  8. data/lib/active_record/associations/builder/association.rb +14 -18
  9. data/lib/active_record/associations/builder/belongs_to.rb +5 -2
  10. data/lib/active_record/associations/builder/collection_association.rb +5 -15
  11. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +1 -1
  12. data/lib/active_record/associations/builder/has_many.rb +2 -0
  13. data/lib/active_record/associations/builder/has_one.rb +35 -1
  14. data/lib/active_record/associations/builder/singular_association.rb +2 -0
  15. data/lib/active_record/associations/collection_association.rb +5 -6
  16. data/lib/active_record/associations/collection_proxy.rb +13 -42
  17. data/lib/active_record/associations/has_many_association.rb +1 -9
  18. data/lib/active_record/associations/has_many_through_association.rb +4 -11
  19. data/lib/active_record/associations/join_dependency.rb +14 -9
  20. data/lib/active_record/associations/join_dependency/join_association.rb +21 -7
  21. data/lib/active_record/associations/preloader.rb +12 -7
  22. data/lib/active_record/associations/preloader/association.rb +37 -34
  23. data/lib/active_record/associations/preloader/through_association.rb +48 -39
  24. data/lib/active_record/attribute_methods.rb +3 -53
  25. data/lib/active_record/attribute_methods/before_type_cast.rb +4 -1
  26. data/lib/active_record/attribute_methods/dirty.rb +47 -14
  27. data/lib/active_record/attribute_methods/primary_key.rb +7 -15
  28. data/lib/active_record/attribute_methods/query.rb +2 -3
  29. data/lib/active_record/attribute_methods/read.rb +3 -9
  30. data/lib/active_record/attribute_methods/write.rb +6 -12
  31. data/lib/active_record/attributes.rb +13 -0
  32. data/lib/active_record/autosave_association.rb +21 -7
  33. data/lib/active_record/base.rb +0 -1
  34. data/lib/active_record/callbacks.rb +3 -3
  35. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +134 -23
  36. data/lib/active_record/connection_adapters/abstract/database_limits.rb +8 -4
  37. data/lib/active_record/connection_adapters/abstract/database_statements.rb +105 -70
  38. data/lib/active_record/connection_adapters/abstract/query_cache.rb +12 -5
  39. data/lib/active_record/connection_adapters/abstract/quoting.rb +63 -6
  40. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +5 -2
  41. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +51 -40
  42. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +1 -1
  43. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +95 -30
  44. data/lib/active_record/connection_adapters/abstract/transaction.rb +17 -6
  45. data/lib/active_record/connection_adapters/abstract_adapter.rb +115 -35
  46. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +106 -138
  47. data/lib/active_record/connection_adapters/column.rb +17 -13
  48. data/lib/active_record/connection_adapters/connection_specification.rb +2 -2
  49. data/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb +3 -3
  50. data/lib/active_record/connection_adapters/mysql/database_statements.rb +48 -8
  51. data/lib/active_record/connection_adapters/mysql/quoting.rb +44 -7
  52. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +40 -32
  53. data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +14 -6
  54. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +66 -5
  55. data/lib/active_record/connection_adapters/mysql/type_metadata.rb +6 -10
  56. data/lib/active_record/connection_adapters/mysql2_adapter.rb +18 -5
  57. data/lib/active_record/connection_adapters/postgresql/column.rb +17 -30
  58. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +8 -2
  59. data/lib/active_record/connection_adapters/postgresql/oid/money.rb +2 -2
  60. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +1 -1
  61. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +6 -3
  62. data/lib/active_record/connection_adapters/postgresql/quoting.rb +40 -3
  63. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +36 -0
  64. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +98 -89
  65. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +47 -63
  66. data/lib/active_record/connection_adapters/postgresql/type_metadata.rb +23 -27
  67. data/lib/active_record/connection_adapters/postgresql_adapter.rb +95 -24
  68. data/lib/active_record/connection_adapters/schema_cache.rb +32 -14
  69. data/lib/active_record/connection_adapters/sql_type_metadata.rb +11 -8
  70. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +120 -0
  71. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +38 -2
  72. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +28 -2
  73. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +73 -118
  74. data/lib/active_record/connection_handling.rb +40 -17
  75. data/lib/active_record/core.rb +35 -24
  76. data/lib/active_record/database_configurations.rb +99 -50
  77. data/lib/active_record/database_configurations/hash_config.rb +11 -11
  78. data/lib/active_record/database_configurations/url_config.rb +21 -16
  79. data/lib/active_record/dynamic_matchers.rb +1 -1
  80. data/lib/active_record/enum.rb +15 -0
  81. data/lib/active_record/errors.rb +18 -13
  82. data/lib/active_record/fixtures.rb +11 -6
  83. data/lib/active_record/gem_version.rb +2 -2
  84. data/lib/active_record/inheritance.rb +1 -1
  85. data/lib/active_record/insert_all.rb +179 -0
  86. data/lib/active_record/integration.rb +13 -1
  87. data/lib/active_record/internal_metadata.rb +5 -1
  88. data/lib/active_record/locking/optimistic.rb +3 -4
  89. data/lib/active_record/log_subscriber.rb +1 -1
  90. data/lib/active_record/middleware/database_selector.rb +75 -0
  91. data/lib/active_record/middleware/database_selector/resolver.rb +88 -0
  92. data/lib/active_record/middleware/database_selector/resolver/session.rb +45 -0
  93. data/lib/active_record/migration.rb +62 -44
  94. data/lib/active_record/migration/command_recorder.rb +28 -14
  95. data/lib/active_record/migration/compatibility.rb +72 -63
  96. data/lib/active_record/model_schema.rb +3 -0
  97. data/lib/active_record/persistence.rb +212 -19
  98. data/lib/active_record/querying.rb +18 -14
  99. data/lib/active_record/railtie.rb +9 -1
  100. data/lib/active_record/railties/collection_cache_association_loading.rb +3 -3
  101. data/lib/active_record/railties/databases.rake +124 -25
  102. data/lib/active_record/reflection.rb +18 -32
  103. data/lib/active_record/relation.rb +185 -35
  104. data/lib/active_record/relation/calculations.rb +40 -44
  105. data/lib/active_record/relation/delegation.rb +23 -31
  106. data/lib/active_record/relation/finder_methods.rb +23 -14
  107. data/lib/active_record/relation/merger.rb +11 -16
  108. data/lib/active_record/relation/query_attribute.rb +5 -3
  109. data/lib/active_record/relation/query_methods.rb +230 -69
  110. data/lib/active_record/relation/spawn_methods.rb +1 -1
  111. data/lib/active_record/relation/where_clause.rb +10 -10
  112. data/lib/active_record/sanitization.rb +33 -4
  113. data/lib/active_record/schema.rb +1 -1
  114. data/lib/active_record/schema_dumper.rb +10 -1
  115. data/lib/active_record/schema_migration.rb +1 -1
  116. data/lib/active_record/scoping.rb +6 -7
  117. data/lib/active_record/scoping/default.rb +7 -15
  118. data/lib/active_record/scoping/named.rb +10 -2
  119. data/lib/active_record/statement_cache.rb +2 -2
  120. data/lib/active_record/store.rb +48 -0
  121. data/lib/active_record/table_metadata.rb +9 -13
  122. data/lib/active_record/tasks/database_tasks.rb +109 -6
  123. data/lib/active_record/tasks/mysql_database_tasks.rb +3 -1
  124. data/lib/active_record/test_databases.rb +1 -16
  125. data/lib/active_record/test_fixtures.rb +2 -2
  126. data/lib/active_record/timestamp.rb +35 -19
  127. data/lib/active_record/touch_later.rb +4 -2
  128. data/lib/active_record/transactions.rb +56 -46
  129. data/lib/active_record/type_caster/connection.rb +16 -10
  130. data/lib/active_record/validations.rb +1 -0
  131. data/lib/active_record/validations/uniqueness.rb +4 -4
  132. data/lib/arel.rb +18 -4
  133. data/lib/arel/insert_manager.rb +3 -3
  134. data/lib/arel/nodes.rb +2 -1
  135. data/lib/arel/nodes/and.rb +1 -1
  136. data/lib/arel/nodes/case.rb +1 -1
  137. data/lib/arel/nodes/comment.rb +29 -0
  138. data/lib/arel/nodes/select_core.rb +16 -12
  139. data/lib/arel/nodes/unary.rb +1 -0
  140. data/lib/arel/nodes/values_list.rb +2 -17
  141. data/lib/arel/select_manager.rb +10 -10
  142. data/lib/arel/visitors/depth_first.rb +7 -2
  143. data/lib/arel/visitors/dot.rb +7 -2
  144. data/lib/arel/visitors/ibm_db.rb +13 -0
  145. data/lib/arel/visitors/informix.rb +6 -0
  146. data/lib/arel/visitors/mssql.rb +15 -1
  147. data/lib/arel/visitors/oracle12.rb +4 -5
  148. data/lib/arel/visitors/postgresql.rb +4 -10
  149. data/lib/arel/visitors/to_sql.rb +107 -131
  150. data/lib/arel/visitors/visitor.rb +9 -5
  151. data/lib/rails/generators/active_record/migration/migration_generator.rb +1 -1
  152. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +1 -1
  153. data/lib/rails/generators/active_record/migration/templates/migration.rb.tt +4 -2
  154. data/lib/rails/generators/active_record/model/model_generator.rb +1 -1
  155. data/lib/rails/generators/active_record/model/templates/model.rb.tt +10 -1
  156. metadata +19 -12
  157. data/lib/active_record/collection_cache_key.rb +0 -53
  158. data/lib/arel/nodes/values.rb +0 -16
@@ -85,14 +85,14 @@ module ActiveRecord
85
85
  # based on the requested role:
86
86
  #
87
87
  # ActiveRecord::Base.connected_to(role: :writing) do
88
- # Dog.create! # creates dog using dog connection
88
+ # Dog.create! # creates dog using dog writing connection
89
89
  # end
90
90
  #
91
91
  # ActiveRecord::Base.connected_to(role: :reading) do
92
92
  # Dog.create! # throws exception because we're on a replica
93
93
  # end
94
94
  #
95
- # ActiveRecord::Base.connected_to(role: :unknown_ode) do
95
+ # ActiveRecord::Base.connected_to(role: :unknown_role) do
96
96
  # # raises exception due to non-existent role
97
97
  # end
98
98
  #
@@ -100,31 +100,44 @@ module ActiveRecord
100
100
  # you can use +connected_to+ with a +database+ argument. The +database+ argument
101
101
  # expects a symbol that corresponds to the database key in your config.
102
102
  #
103
- # This will connect to a new database for the queries inside the block.
104
- #
105
103
  # ActiveRecord::Base.connected_to(database: :animals_slow_replica) do
106
104
  # Dog.run_a_long_query # runs a long query while connected to the +animals_slow_replica+
107
105
  # end
108
- def connected_to(database: nil, role: nil, &blk)
106
+ #
107
+ # This will connect to a new database for the queries inside the block. By
108
+ # default the `:writing` role will be used since all connections must be assigned
109
+ # a role. If you would like to use a different role you can pass a hash to database:
110
+ #
111
+ # ActiveRecord::Base.connected_to(database: { readonly_slow: :animals_slow_replica }) do
112
+ # # runs a long query while connected to the +animals_slow_replica+ using the readonly_slow role.
113
+ # Dog.run_a_long_query
114
+ # end
115
+ #
116
+ # When using the database key a new connection will be established every time. It is not
117
+ # recommended to use this outside of one-off scripts.
118
+ def connected_to(database: nil, role: nil, prevent_writes: false, &blk)
109
119
  if database && role
110
120
  raise ArgumentError, "connected_to can only accept a `database` or a `role` argument, but not both arguments."
111
121
  elsif database
112
122
  if database.is_a?(Hash)
113
123
  role, database = database.first
114
124
  role = role.to_sym
115
- else
116
- role = database.to_sym
117
125
  end
118
126
 
119
127
  config_hash = resolve_config_for_connection(database)
120
128
  handler = lookup_connection_handler(role)
121
129
 
122
- with_handler(role) do
123
- handler.establish_connection(config_hash)
124
- yield
125
- end
130
+ handler.establish_connection(config_hash)
131
+
132
+ with_handler(role, &blk)
126
133
  elsif role
127
- with_handler(role.to_sym, &blk)
134
+ if role == writing_role
135
+ with_handler(role.to_sym) do
136
+ connection_handler.while_preventing_writes(prevent_writes, &blk)
137
+ end
138
+ else
139
+ with_handler(role.to_sym, &blk)
140
+ end
128
141
  else
129
142
  raise ArgumentError, "must provide a `database` or a `role`."
130
143
  end
@@ -154,14 +167,11 @@ module ActiveRecord
154
167
  end
155
168
 
156
169
  def lookup_connection_handler(handler_key) # :nodoc:
170
+ handler_key ||= ActiveRecord::Base.writing_role
157
171
  connection_handlers[handler_key] ||= ActiveRecord::ConnectionAdapters::ConnectionHandler.new
158
172
  end
159
173
 
160
174
  def with_handler(handler_key, &blk) # :nodoc:
161
- unless ActiveRecord::Base.connection_handlers.keys.include?(handler_key)
162
- raise ArgumentError, "The #{handler_key} role does not exist. Add it by establishing a connection with `connects_to` or use an existing role (#{ActiveRecord::Base.connection_handlers.keys.join(", ")})."
163
- end
164
-
165
175
  handler = lookup_connection_handler(handler_key)
166
176
  swap_connection_handler(handler, &blk)
167
177
  end
@@ -170,7 +180,7 @@ module ActiveRecord
170
180
  raise "Anonymous class is not allowed." unless name
171
181
 
172
182
  config_or_env ||= DEFAULT_ENV.call.to_sym
173
- pool_name = self == Base ? "primary" : name
183
+ pool_name = primary_class? ? "primary" : name
174
184
  self.connection_specification_name = pool_name
175
185
 
176
186
  resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(Base.configurations)
@@ -180,6 +190,15 @@ module ActiveRecord
180
190
  config_hash
181
191
  end
182
192
 
193
+ # Clears the query cache for all connections associated with the current thread.
194
+ def clear_query_caches_for_current_thread
195
+ ActiveRecord::Base.connection_handlers.each_value do |handler|
196
+ handler.connection_pool_list.each do |pool|
197
+ pool.connection.clear_query_cache if pool.active_connection?
198
+ end
199
+ end
200
+ end
201
+
183
202
  # Returns the connection currently associated with the class. This can
184
203
  # also be used to "borrow" the connection to do database work unrelated
185
204
  # to any of the specific Active Records.
@@ -197,6 +216,10 @@ module ActiveRecord
197
216
  @connection_specification_name
198
217
  end
199
218
 
219
+ def primary_class? # :nodoc:
220
+ self == Base || defined?(ApplicationRecord) && self == ApplicationRecord
221
+ end
222
+
200
223
  # Returns the configuration of the associated connection as a hash:
201
224
  #
202
225
  # ActiveRecord::Base.connection_config
@@ -124,6 +124,10 @@ module ActiveRecord
124
124
 
125
125
  mattr_accessor :connection_handlers, instance_accessor: false, default: {}
126
126
 
127
+ mattr_accessor :writing_role, instance_accessor: false, default: :writing
128
+
129
+ mattr_accessor :reading_role, instance_accessor: false, default: :reading
130
+
127
131
  class_attribute :default_connection_handler, instance_writer: false
128
132
 
129
133
  self.filter_attributes = []
@@ -137,7 +141,6 @@ module ActiveRecord
137
141
  end
138
142
 
139
143
  self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new
140
- self.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
141
144
  end
142
145
 
143
146
  module ClassMethods
@@ -157,7 +160,7 @@ module ActiveRecord
157
160
  return super if block_given? ||
158
161
  primary_key.nil? ||
159
162
  scope_attributes? ||
160
- columns_hash.include?(inheritance_column)
163
+ columns_hash.key?(inheritance_column) && !base_class?
161
164
 
162
165
  id = ids.first
163
166
 
@@ -171,14 +174,14 @@ module ActiveRecord
171
174
 
172
175
  record = statement.execute([id], connection)&.first
173
176
  unless record
174
- raise RecordNotFound.new("Couldn't find #{name} with '#{primary_key}'=#{id}",
175
- name, primary_key, id)
177
+ raise RecordNotFound.new("Couldn't find #{name} with '#{key}'=#{id}", name, key, id)
176
178
  end
177
179
  record
178
180
  end
179
181
 
180
182
  def find_by(*args) # :nodoc:
181
- return super if scope_attributes? || reflect_on_all_aggregations.any?
183
+ return super if scope_attributes? || reflect_on_all_aggregations.any? ||
184
+ columns_hash.key?(inheritance_column) && !base_class?
182
185
 
183
186
  hash = args.first
184
187
 
@@ -265,7 +268,8 @@ module ActiveRecord
265
268
  end
266
269
 
267
270
  def arel_attribute(name, table = arel_table) # :nodoc:
268
- name = attribute_alias(name) if attribute_alias?(name)
271
+ name = name.to_s
272
+ name = attribute_aliases[name] || name
269
273
  table[name]
270
274
  end
271
275
 
@@ -313,7 +317,7 @@ module ActiveRecord
313
317
  # # Instantiates a single new object
314
318
  # User.new(first_name: 'Jamie')
315
319
  def initialize(attributes = nil)
316
- self.class.define_attribute_methods
320
+ @new_record = true
317
321
  @attributes = self.class._default_attributes.deep_dup
318
322
 
319
323
  init_internals
@@ -350,12 +354,10 @@ module ActiveRecord
350
354
  # +attributes+ should be an attributes object, and unlike the
351
355
  # `initialize` method, no assignment calls are made per attribute.
352
356
  def init_with_attributes(attributes, new_record = false) # :nodoc:
353
- init_internals
354
-
355
357
  @new_record = new_record
356
358
  @attributes = attributes
357
359
 
358
- self.class.define_attribute_methods
360
+ init_internals
359
361
 
360
362
  yield self if block_given?
361
363
 
@@ -394,13 +396,13 @@ module ActiveRecord
394
396
  ##
395
397
  def initialize_dup(other) # :nodoc:
396
398
  @attributes = @attributes.deep_dup
397
- @attributes.reset(self.class.primary_key)
399
+ @attributes.reset(@primary_key)
398
400
 
399
401
  _run_initialize_callbacks
400
402
 
401
403
  @new_record = true
402
404
  @destroyed = false
403
- @_start_transaction_state = {}
405
+ @_start_transaction_state = nil
404
406
  @transaction_state = nil
405
407
 
406
408
  super
@@ -461,6 +463,7 @@ module ActiveRecord
461
463
 
462
464
  # Returns +true+ if the attributes hash has been frozen.
463
465
  def frozen?
466
+ sync_with_transaction_state if @transaction_state&.finalized?
464
467
  @attributes.frozen?
465
468
  end
466
469
 
@@ -473,6 +476,14 @@ module ActiveRecord
473
476
  end
474
477
  end
475
478
 
479
+ def present? # :nodoc:
480
+ true
481
+ end
482
+
483
+ def blank? # :nodoc:
484
+ false
485
+ end
486
+
476
487
  # Returns +true+ if the record is read only. Records loaded through joins with piggy-back
477
488
  # attributes will be marked as read only since they cannot be saved.
478
489
  def readonly?
@@ -557,34 +568,34 @@ module ActiveRecord
557
568
  end
558
569
 
559
570
  def init_internals
571
+ @primary_key = self.class.primary_key
560
572
  @readonly = false
561
573
  @destroyed = false
562
574
  @marked_for_destruction = false
563
575
  @destroyed_by_association = nil
564
- @new_record = true
565
- @_start_transaction_state = {}
576
+ @_start_transaction_state = nil
566
577
  @transaction_state = nil
567
- end
568
578
 
569
- def initialize_internals_callback
579
+ self.class.define_attribute_methods
570
580
  end
571
581
 
572
- def thaw
573
- if frozen?
574
- @attributes = @attributes.dup
575
- end
582
+ def initialize_internals_callback
576
583
  end
577
584
 
578
585
  def custom_inspect_method_defined?
579
586
  self.class.instance_method(:inspect).owner != ActiveRecord::Base.instance_method(:inspect).owner
580
587
  end
581
588
 
589
+ class InspectionMask < DelegateClass(::String)
590
+ def pretty_print(pp)
591
+ pp.text __getobj__
592
+ end
593
+ end
594
+ private_constant :InspectionMask
595
+
582
596
  def inspection_filter
583
597
  @inspection_filter ||= begin
584
- mask = DelegateClass(::String).new(ActiveSupport::ParameterFilter::FILTERED)
585
- def mask.pretty_print(pp)
586
- pp.text __getobj__
587
- end
598
+ mask = InspectionMask.new(ActiveSupport::ParameterFilter::FILTERED)
588
599
  ActiveSupport::ParameterFilter.new(self.class.filter_attributes, mask: mask)
589
600
  end
590
601
  end
@@ -7,8 +7,10 @@ require "active_record/database_configurations/url_config"
7
7
  module ActiveRecord
8
8
  # ActiveRecord::DatabaseConfigurations returns an array of DatabaseConfig
9
9
  # objects (either a HashConfig or UrlConfig) that are constructed from the
10
- # application's database configuration hash or url string.
10
+ # application's database configuration hash or URL string.
11
11
  class DatabaseConfigurations
12
+ class InvalidConfigurationError < StandardError; end
13
+
12
14
  attr_reader :configurations
13
15
  delegate :any?, to: :configurations
14
16
 
@@ -17,22 +19,22 @@ module ActiveRecord
17
19
  end
18
20
 
19
21
  # Collects the configs for the environment and optionally the specification
20
- # name passed in. To include replica configurations pass `include_replicas: true`.
22
+ # name passed in. To include replica configurations pass <tt>include_replicas: true</tt>.
21
23
  #
22
24
  # If a spec name is provided a single DatabaseConfig object will be
23
25
  # returned, otherwise an array of DatabaseConfig objects will be
24
26
  # returned that corresponds with the environment and type requested.
25
27
  #
26
- # Options:
28
+ # ==== Options
27
29
  #
28
- # <tt>env_name:</tt> The environment name. Defaults to nil which will collect
29
- # configs for all environments.
30
- # <tt>spec_name:</tt> The specification name (ie primary, animals, etc.). Defaults
31
- # to +nil+.
32
- # <tt>include_replicas:</tt> Determines whether to include replicas in
33
- # the returned list. Most of the time we're only iterating over the write
34
- # connection (i.e. migrations don't need to run for the write and read connection).
35
- # Defaults to +false+.
30
+ # * <tt>env_name:</tt> The environment name. Defaults to +nil+ which will collect
31
+ # configs for all environments.
32
+ # * <tt>spec_name:</tt> The specification name (i.e. primary, animals, etc.). Defaults
33
+ # to +nil+.
34
+ # * <tt>include_replicas:</tt> Determines whether to include replicas in
35
+ # the returned list. Most of the time we're only iterating over the write
36
+ # connection (i.e. migrations don't need to run for the write and read connection).
37
+ # Defaults to +false+.
36
38
  def configs_for(env_name: nil, spec_name: nil, include_replicas: false)
37
39
  configs = env_with_configs(env_name)
38
40
 
@@ -53,7 +55,7 @@ module ActiveRecord
53
55
 
54
56
  # Returns the config hash that corresponds with the environment
55
57
  #
56
- # If the application has multiple databases `default_hash` will
58
+ # If the application has multiple databases +default_hash+ will
57
59
  # return the first config hash for the environment.
58
60
  #
59
61
  # { database: "my_db", adapter: "mysql2" }
@@ -65,7 +67,7 @@ module ActiveRecord
65
67
 
66
68
  # Returns a single DatabaseConfig object based on the requested environment.
67
69
  #
68
- # If the application has multiple databases `find_db_config` will return
70
+ # If the application has multiple databases +find_db_config+ will return
69
71
  # the first DatabaseConfig for the environment.
70
72
  def find_db_config(env)
71
73
  configurations.find do |db_config|
@@ -91,6 +93,19 @@ module ActiveRecord
91
93
  end
92
94
  alias :blank? :empty?
93
95
 
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]
107
+ end
108
+
94
109
  private
95
110
  def env_with_configs(env = nil)
96
111
  if env
@@ -102,83 +117,117 @@ module ActiveRecord
102
117
 
103
118
  def build_configs(configs)
104
119
  return configs.configurations if configs.is_a?(DatabaseConfigurations)
120
+ return configs if configs.is_a?(Array)
105
121
 
106
- build_db_config = configs.each_pair.flat_map do |env_name, config|
107
- walk_configs(env_name.to_s, "primary", config)
108
- end.compact
122
+ db_configs = configs.flat_map do |env_name, config|
123
+ if config.is_a?(Hash) && config.all? { |_, v| v.is_a?(Hash) }
124
+ walk_configs(env_name.to_s, config)
125
+ else
126
+ build_db_config_from_raw_config(env_name.to_s, "primary", config)
127
+ end
128
+ end
109
129
 
110
- if url = ENV["DATABASE_URL"]
111
- build_url_config(url, build_db_config)
112
- else
113
- build_db_config
130
+ current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s
131
+
132
+ unless db_configs.find(&:for_current_env?)
133
+ db_configs << environment_url_config(current_env, "primary", {})
114
134
  end
135
+
136
+ merge_db_environment_variables(current_env, db_configs.compact)
115
137
  end
116
138
 
117
- def walk_configs(env_name, spec_name, config)
139
+ 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)
142
+ end
143
+ end
144
+
145
+ def build_db_config_from_raw_config(env_name, spec_name, config)
118
146
  case config
119
147
  when String
120
148
  build_db_config_from_string(env_name, spec_name, config)
121
149
  when Hash
122
150
  build_db_config_from_hash(env_name, spec_name, config.stringify_keys)
151
+ else
152
+ raise InvalidConfigurationError, "'{ #{env_name} => #{config} }' is not a valid configuration. Expected '#{config}' to be a URL string or a Hash."
123
153
  end
124
154
  end
125
155
 
126
156
  def build_db_config_from_string(env_name, spec_name, config)
127
157
  url = config
128
158
  uri = URI.parse(url)
129
- if uri.try(:scheme)
159
+ if uri.scheme
130
160
  ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, spec_name, url)
161
+ else
162
+ raise InvalidConfigurationError, "'{ #{env_name} => #{config} }' is not a valid configuration. Expected '#{config}' to be a URL string or a Hash."
131
163
  end
132
- rescue URI::InvalidURIError
133
- ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config)
134
164
  end
135
165
 
136
166
  def build_db_config_from_hash(env_name, spec_name, config)
137
- if url = config["url"]
167
+ if config.has_key?("url")
168
+ url = config["url"]
138
169
  config_without_url = config.dup
139
170
  config_without_url.delete "url"
171
+
140
172
  ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, spec_name, url, config_without_url)
141
- elsif config["database"] || (config.size == 1 && config.values.all? { |v| v.is_a? String })
142
- ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config)
143
173
  else
144
- config.each_pair.map do |sub_spec_name, sub_config|
145
- walk_configs(env_name, sub_spec_name, sub_config)
146
- end
174
+ ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config)
147
175
  end
148
176
  end
149
177
 
150
- def build_url_config(url, configs)
151
- env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s
178
+ def merge_db_environment_variables(current_env, configs)
179
+ configs.map do |config|
180
+ next config if config.url_config? || config.env_name != current_env
152
181
 
153
- if original_config = configs.find(&:for_current_env?)
154
- if original_config.url_config?
155
- configs
156
- else
157
- configs.map do |config|
158
- ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, config.spec_name, url, config.config)
159
- end
160
- end
161
- else
162
- configs + [ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, "primary", url)]
182
+ url_config = environment_url_config(current_env, config.spec_name, config.config)
183
+ url_config || config
163
184
  end
164
185
  end
165
186
 
166
- def method_missing(method, *args, &blk)
167
- if Hash.method_defined?(method)
168
- ActiveSupport::Deprecation.warn \
169
- "Returning a hash from ActiveRecord::Base.configurations is deprecated. Therefore calling `#{method}` on the hash is also deprecated. Please switch to using the `configs_for` method instead to collect and iterate over database configurations."
170
- end
187
+ def environment_url_config(env, spec_name, config)
188
+ url = environment_value_for(spec_name)
189
+ return unless url
190
+
191
+ ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, spec_name, url, config)
192
+ end
193
+
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"
198
+ url
199
+ end
171
200
 
201
+ def method_missing(method, *args, &blk)
172
202
  case method
173
- when :each, :first
174
- configurations.send(method, *args, &blk)
175
203
  when :fetch
204
+ throw_getter_deprecation(method)
176
205
  configs_for(env_name: args.first)
177
206
  when :values
207
+ throw_getter_deprecation(method)
178
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
179
220
  else
180
- super
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."
181
222
  end
182
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
183
232
  end
184
233
  end