rails / rails

Ruby on Rails
https://rubyonrails.org
MIT License
55.93k stars 21.64k forks source link

database.yml gets evaluated before initializers run since Rails 7.1.0 #51578

Closed 2called-chaos closed 6 months ago

2called-chaos commented 6 months ago

Steps to reproduce

  1. Add to database.yml <% puts "database.yml" %>
  2. Create initializers/foo.rb with puts "initializer"
  3. Run rails db:migrate or rails db:create on <7.1 (e.g. 7.0.8.1) and >= 7.1.0

Expected behavior (as was the case <7.1)

Initializers run before database.yml gets interpreted by ERB.

initializer
database.yml

Actual behavior (since 7.1)

Initializers now run after database.yml has been interpreted by ERB

database.yml
initializer

System configuration

Rails version: 7.0.8.1, 7.1.0, 7.1.3.2

Ruby version: 3.3.0


Not sure if this is intended but as far as I can tell not documented. This obviously breaks if you attempt to use config data sourced and/or configured by an initializer in your database.yml.

justinko commented 6 months ago

This is expected.

Prior to 7.1, if you had multiple databases in an environment (e.g. shard_1, shard_2), and you ran bin/rake -T, you wouldn't see tasks such as rake db:create:shard_1 because the environment was never loaded therefore database.yml was never loaded.

Rails 7.1 gets around this by loading database.yml with a fake/dummy app config (not the real environment) and when you run bin/rake -T you'll see:

rake db:create:shard_1                  # Create shard_1 database for current environment
rake db:create:shard_2                  # Create shard_2 database for current environment

When you actually run a task like db:create in Rails 7.1, it'll load database.yml with the dummy app config, load the actual app, and then load database.yml again with the non-dummy/real app config.

So no, nothing breaks.

2called-chaos commented 6 months ago

Alright so it doesn't matter if the dummy does not get proper credentials? Not sure what the best course of action is for us but it does break if your initializer defines something your yml attempts to use like a config object (that doesn't ignore missing keys).

Like I can use Rails.application.credentials but where would I initialize my custom config if not in an initializer, even environment.rb is too late?

rafaelfranca commented 6 months ago

That is strange. The dummy config thing should not have changed the order. Did you find the commit that changed this behavior?

justinko commented 6 months ago

@rafaelfranca it's not that the order has changed, it's that the ERB is no longer being stripped out (on initial database.yml load). Here's the commit that changes it: https://github.com/rails/rails/pull/46134

rafaelfranca commented 6 months ago

I just wanted to confirm the change that introduced the change of behavior.

@2called-chaos if you custom configuration is inside Rails.application.config things would just work. Closing since this isn't an issue.

justinko commented 6 months ago

@2called-chaos https://guides.rubyonrails.org/configuring.html#custom-configuration

justinko commented 6 months ago

This is expected.

Prior to 7.1, if you had multiple databases in an environment (e.g. shard_1, shard_2), and you ran bin/rake -T, you wouldn't see tasks such as rake db:create:shard_1 because the environment was never loaded therefore database.yml was never loaded.

Rails 7.1 gets around this by loading database.yml with a fake/dummy app config (not the real environment) and when you run bin/rake -T you'll see:

rake db:create:shard_1                  # Create shard_1 database for current environment
rake db:create:shard_2                  # Create shard_2 database for current environment

When you actually run a task like db:create in Rails 7.1, it'll load database.yml with the dummy app config, load the actual app, and then load database.yml again with the non-dummy/real app config.

So no, nothing breaks.

This was totally wrong. I understand it now. Here's the proper explanation:

Let's say you have a constant in an initializer named FOO and you use it as a value in database.yml. Prior to Rails 7.1, this ERB value would get replaced with an empty string. So on the initial database.yml load, you won't get the "uninitialized constant" error.

In Rails 7.1, the ERB values in database.yml are not replaced, so when it is initially loaded, you'll get NameError: uninitialized constant FOO (NameError). The parsing of database.yml is dependent on the interface of Rails config, so it makes sense to use it for custom config values. An alternative would be to place your code after Bundler.require in application.rb.