Skip to content

Commit

Permalink
Load YAML for rake tasks without parsing ERB
Browse files Browse the repository at this point in the history
This change adds a new method that loads the YAML for the database
config without parsing the ERB. This may seem odd but bear with me:

When we added the ability to have rake tasks for multiple databases we
started looping through the configurations to collect the namespaces so
we could do `rake db:create:my_second_db`. See rails#32274.

This caused a problem where if you had `Rails.config.max_threads` set in
your database.yml it will blow up because the environment that defines
`max_threads` isn't loaded during `rake -T`. See rails#35468.

We tried to fix this by adding the ability to just load the YAML and
ignore ERB all together but that caused a bug in GitHub's YAML loading
where if you used multi-line ERB the YAML was invalid. That led us to
reverting some changes in rails#33748.

After trying to resolve this a bunch of ways `@tenderlove` came up with
replacing the ERB values so that we don't need to load the environment
but we also can load the YAML.

This change adds a DummyCompiler for ERB that will replace all the
values so we can load the database yaml and create the rake tasks.
Nothing else uses this method so it's "safe".

DO NOT use this method in your application.

Fixes rails#35468
  • Loading branch information
eileencodes committed Mar 6, 2019
1 parent db94f49 commit 37d1429
Show file tree
Hide file tree
Showing 4 changed files with 40 additions and 13 deletions.
2 changes: 1 addition & 1 deletion activerecord/lib/active_record/tasks/database_tasks.rb
Expand Up @@ -142,7 +142,7 @@ def create_all
end

def for_each
databases = Rails.application.config.database_configuration
databases = Rails.application.config.load_database_yaml
database_configs = ActiveRecord::DatabaseConfigurations.new(databases).configs_for(env_name: Rails.env)

# if this is a single database application we don't want tasks for each primary database
Expand Down
20 changes: 20 additions & 0 deletions railties/lib/rails/application/configuration.rb
Expand Up @@ -184,6 +184,26 @@ def paths
end
end

# Load the database YAML without evaluating ERB. This allows us to
# create the rake tasks for multiple databases without filling in the
# configuration values or loading the environment. Do not use this
# method.
#
# This uses a DummyERB custom compiler so YAML can ignore the ERB
# tags and load the database.yml for the rake tasks.
def load_database_yaml # :nodoc:
if path = paths["config/database"].existent.first
require "rails/application/dummy_erb_compiler"

yaml = Pathname.new(path)
erb = DummyERB.new(yaml.read)

YAML.load(erb.result)
else
{}
end
end

# Loads and returns the entire raw configuration of database from
# values stored in <tt>config/database.yml</tt>.
def database_configuration
Expand Down
16 changes: 16 additions & 0 deletions railties/lib/rails/application/dummy_erb_compiler.rb
@@ -0,0 +1,16 @@
# frozen_string_literal: true

# These classes are used to strip out the ERB configuration
# values so we can evaluate the database.yml without evaluating
# the ERB values.
class DummyERB < ERB # :nodoc:
def make_compiler(trim_mode)
DummyCompiler.new trim_mode
end
end

class DummyCompiler < ERB::Compiler # :nodoc:
def compile_content(stag, out)
out.push "_erbout << 'dummy_compiler'"
end
end
15 changes: 3 additions & 12 deletions railties/test/application/rake/dbs_test.rb
Expand Up @@ -54,26 +54,17 @@ def db_create_and_drop(expected_database, environment_loaded: true)
test "db:create and db:drop respect environment setting" do
app_file "config/database.yml", <<-YAML
development:
database: db/development.sqlite3
database: <%= Rails.application.config.database %>
adapter: sqlite3
YAML

app_file "config/environments/development.rb", <<-RUBY
Rails.application.configure do
config.read_encrypted_secrets = true
config.database = "db/development.sqlite3"
end
RUBY

app_file "lib/tasks/check_env.rake", <<-RUBY
Rake::Task["db:create"].enhance do
File.write("tmp/config_value", Rails.application.config.read_encrypted_secrets)
end
RUBY

db_create_and_drop("db/development.sqlite3", environment_loaded: false) do
assert File.exist?("tmp/config_value")
assert_equal "true", File.read("tmp/config_value")
end
db_create_and_drop("db/development.sqlite3", environment_loaded: false)
end

def with_database_existing
Expand Down

0 comments on commit 37d1429

Please sign in to comment.