fixed support for DATABASE_URL for rake db tasks

- added tests to confirm establish_connection uses DATABASE_URL and
  Rails.env correctly even when no arguments are passed in.
- updated rake db tasks to support DATABASE_URL, and added tests to
  confirm correct behavior for these rake tasks.  (Removed
  establish_connection call from some tasks since in those cases
  the :environment task already made sure the function would be called)
- updated Resolver so that when it resolves the database url, it
  removes hash values with empty strings from the config spec (e.g.
  to support connection to postgresql when no username is specified).
This commit is contained in:
Grace Liu 2012-08-30 18:50:30 -07:00
parent 34b23e7110
commit 148c50b49a
7 changed files with 275 additions and 32 deletions

@ -72,7 +72,7 @@ def connection_url_to_hash(url) # :nodoc:
:port => config.port,
:database => config.path.sub(%r{^/},""),
:host => config.host }
spec.reject!{ |_,value| !value }
spec.reject!{ |_,value| value.blank? }
if config.query
options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys
spec.merge!(options)

@ -18,9 +18,13 @@ db_namespace = namespace :db do
end
end
desc 'Create the database from config/database.yml for the current Rails.env (use db:create:all to create all dbs in the config)'
desc 'Create the database from DATABASE_URL or config/database.yml for the current Rails.env (use db:create:all to create all dbs in the config)'
task :create => [:load_config] do
ActiveRecord::Tasks::DatabaseTasks.create_current
if ENV['DATABASE_URL']
ActiveRecord::Tasks::DatabaseTasks.create_database_url
else
ActiveRecord::Tasks::DatabaseTasks.create_current
end
end
namespace :drop do
@ -29,9 +33,13 @@ db_namespace = namespace :db do
end
end
desc 'Drops the database for the current Rails.env (use db:drop:all to drop all databases)'
desc 'Drops the database using DATABASE_URL or the current Rails.env (use db:drop:all to drop all databases)'
task :drop => [:load_config] do
ActiveRecord::Tasks::DatabaseTasks.drop_current
if ENV['DATABASE_URL']
ActiveRecord::Tasks::DatabaseTasks.drop_database_url
else
ActiveRecord::Tasks::DatabaseTasks.drop_current
end
end
desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)."
@ -88,8 +96,6 @@ db_namespace = namespace :db do
desc 'Display status of migrations'
task :status => [:environment, :load_config] do
config = ActiveRecord::Base.configurations[Rails.env]
ActiveRecord::Base.establish_connection(config)
unless ActiveRecord::Base.connection.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name)
puts 'Schema migrations table does not exist yet.'
next # means "return" for rake task
@ -110,7 +116,7 @@ db_namespace = namespace :db do
['up', version, '********** NO FILE **********']
end
# output
puts "\ndatabase: #{config['database']}\n\n"
puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n"
puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name"
puts "-" * 50
(db_list + file_list).sort_by {|migration| migration[1]}.each do |migration|
@ -186,7 +192,6 @@ db_namespace = namespace :db do
task :load => [:environment, :load_config] do
require 'active_record/fixtures'
ActiveRecord::Base.establish_connection(Rails.env)
base_dir = File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten
fixtures_dir = File.join [base_dir, ENV['FIXTURES_DIR']].compact
@ -225,7 +230,6 @@ db_namespace = namespace :db do
require 'active_record/schema_dumper'
filename = ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb"
File.open(filename, "w:utf-8") do |file|
ActiveRecord::Base.establish_connection(Rails.env)
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
end
db_namespace['schema:dump'].reenable
@ -277,22 +281,22 @@ db_namespace = namespace :db do
desc 'Dump the database structure to db/structure.sql. Specify another file with DB_STRUCTURE=db/my_structure.sql'
task :dump => [:environment, :load_config] do
abcs = ActiveRecord::Base.configurations
filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql")
case abcs[Rails.env]['adapter']
current_config = ActiveRecord::Tasks::DatabaseTasks.current_config
case current_config['adapter']
when /mysql/, /postgresql/, /sqlite/
ActiveRecord::Tasks::DatabaseTasks.structure_dump(abcs[Rails.env], filename)
ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename)
when 'oci', 'oracle'
ActiveRecord::Base.establish_connection(abcs[Rails.env])
ActiveRecord::Base.establish_connection(current_config)
File.open(filename, "w:utf-8") { |f| f << ActiveRecord::Base.connection.structure_dump }
when 'sqlserver'
`smoscript -s #{abcs[Rails.env]['host']} -d #{abcs[Rails.env]['database']} -u #{abcs[Rails.env]['username']} -p #{abcs[Rails.env]['password']} -f #{filename} -A -U`
`smoscript -s #{current_config['host']} -d #{current_config['database']} -u #{current_config['username']} -p #{current_config['password']} -f #{filename} -A -U`
when "firebird"
set_firebird_env(abcs[Rails.env])
db_string = firebird_db_string(abcs[Rails.env])
set_firebird_env(current_config)
db_string = firebird_db_string(current_config)
sh "isql -a #{db_string} > #{filename}"
else
raise "Task not supported by '#{abcs[Rails.env]["adapter"]}'"
raise "Task not supported by '#{current_config["adapter"]}'"
end
if ActiveRecord::Base.connection.supports_migrations?
@ -303,26 +307,24 @@ db_namespace = namespace :db do
# desc "Recreate the databases from the structure.sql file"
task :load => [:environment, :load_config] do
env = ENV['RAILS_ENV'] || 'test'
abcs = ActiveRecord::Base.configurations
current_config = ActiveRecord::Tasks::DatabaseTasks.current_config(:env => (ENV['RAILS_ENV'] || 'test'))
filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql")
case abcs[env]['adapter']
case current_config['adapter']
when /mysql/, /postgresql/, /sqlite/
ActiveRecord::Tasks::DatabaseTasks.structure_load(abcs[env], filename)
ActiveRecord::Tasks::DatabaseTasks.structure_load(current_config, filename)
when 'sqlserver'
`sqlcmd -S #{abcs[env]['host']} -d #{abcs[env]['database']} -U #{abcs[env]['username']} -P #{abcs[env]['password']} -i #{filename}`
`sqlcmd -S #{current_config['host']} -d #{current_config['database']} -U #{current_config['username']} -P #{current_config['password']} -i #{filename}`
when 'oci', 'oracle'
ActiveRecord::Base.establish_connection(abcs[env])
ActiveRecord::Base.establish_connection(current_config)
IO.read(filename).split(";\n\n").each do |ddl|
ActiveRecord::Base.connection.execute(ddl)
end
when 'firebird'
set_firebird_env(abcs[env])
db_string = firebird_db_string(abcs[env])
set_firebird_env(current_config)
db_string = firebird_db_string(current_config)
sh "isql -i #{filename} #{db_string}"
else
raise "Task not supported by '#{abcs[env]['adapter']}'"
raise "Task not supported by '#{current_config['adapter']}'"
end
end
@ -353,10 +355,10 @@ db_namespace = namespace :db do
# desc "Recreate the test database from an existent structure.sql file"
task :load_structure => 'db:test:purge' do
begin
old_env, ENV['RAILS_ENV'] = ENV['RAILS_ENV'], 'test'
ActiveRecord::Tasks::DatabaseTasks.current_config(:config => ActiveRecord::Base.configurations['test'])
db_namespace["structure:load"].invoke
ensure
ENV['RAILS_ENV'] = old_env
ActiveRecord::Tasks::DatabaseTasks.current_config(:config => nil)
end
end

@ -3,6 +3,8 @@ module Tasks # :nodoc:
module DatabaseTasks # :nodoc:
extend self
attr_writer :current_config
LOCAL_HOSTS = ['127.0.0.1', 'localhost']
def register_task(pattern, task)
@ -14,6 +16,19 @@ def register_task(pattern, task)
register_task(/postgresql/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks)
register_task(/sqlite/, ActiveRecord::Tasks::SQLiteDatabaseTasks)
def current_config(options = {})
options.reverse_merge! :env => Rails.env
if options.has_key?(:config)
@current_config = options[:config]
else
@current_config ||= if ENV['DATABASE_URL']
database_url_config
else
ActiveRecord::Base.configurations[options[:env]]
end
end
end
def create(*arguments)
configuration = arguments.first
class_for_adapter(configuration['adapter']).new(*arguments).create
@ -33,6 +48,10 @@ def create_current(environment = Rails.env)
ActiveRecord::Base.establish_connection environment
end
def create_database_url
create database_url_config
end
def drop(*arguments)
configuration = arguments.first
class_for_adapter(configuration['adapter']).new(*arguments).drop
@ -51,6 +70,10 @@ def drop_current(environment = Rails.env)
}
end
def drop_database_url
drop database_url_config
end
def charset_current(environment = Rails.env)
charset ActiveRecord::Base.configurations[environment]
end
@ -87,6 +110,11 @@ def structure_load(*arguments)
private
def database_url_config
@database_url_config ||=
ConnectionAdapters::ConnectionSpecification::Resolver.new(ENV["DATABASE_URL"], {}).spec.config.stringify_keys
end
def class_for_adapter(adapter)
key = @tasks.keys.detect { |pattern| adapter[pattern] }
@tasks[key]

@ -13,7 +13,6 @@ def test_url_host_no_db
spec = resolve 'mysql://foo?encoding=utf8'
assert_equal({
:adapter => "mysql",
:database => "",
:host => "foo",
:encoding => "utf8" }, spec)
end
@ -33,7 +32,6 @@ def test_url_port
spec = resolve 'mysql://foo:123?encoding=utf8'
assert_equal({
:adapter => "mysql",
:database => "",
:port => 123,
:host => "foo",
:encoding => "utf8" }, spec)

@ -1,5 +1,7 @@
## Rails 4.0.0 (unreleased) ##
* Fixed support for DATABASE_URL environment variable for rake db tasks. *Grace Liu*
* rails dbconsole now can use SSL for MySQL. The database.yml options sslca, sslcert, sslcapath, sslcipher,
and sslkey now affect rails dbconsole. *Jim Kingdon and Lars Petrus*

@ -195,5 +195,37 @@ def from_bar_helper
assert !ActiveRecord::Base.connection.schema_cache.tables["posts"]
}
end
test "active record establish_connection uses Rails.env if DATABASE_URL is not set" do
begin
require "#{app_path}/config/environment"
orig_database_url = ENV.delete("DATABASE_URL")
orig_rails_env, Rails.env = Rails.env, 'development'
ActiveRecord::Base.establish_connection
assert ActiveRecord::Base.connection
assert_match /#{ActiveRecord::Base.configurations[Rails.env]['database']}/, ActiveRecord::Base.connection_config[:database]
ensure
ActiveRecord::Base.remove_connection
ENV["DATABASE_URL"] = orig_database_url if orig_database_url
Rails.env = orig_rails_env if orig_rails_env
end
end
test "active record establish_connection uses DATABASE_URL even if Rails.env is set" do
begin
require "#{app_path}/config/environment"
orig_database_url = ENV.delete("DATABASE_URL")
orig_rails_env, Rails.env = Rails.env, 'development'
database_url_db_name = "db/database_url_db.sqlite3"
ENV["DATABASE_URL"] = "sqlite3://:@localhost/#{database_url_db_name}"
ActiveRecord::Base.establish_connection
assert ActiveRecord::Base.connection
assert_match /#{database_url_db_name}/, ActiveRecord::Base.connection_config[:database]
ensure
ActiveRecord::Base.remove_connection
ENV["DATABASE_URL"] = orig_database_url if orig_database_url
Rails.env = orig_rails_env if orig_rails_env
end
end
end
end

@ -0,0 +1,181 @@
require "isolation/abstract_unit"
module ApplicationTests
module RakeTests
class RakeDbsTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation
def setup
build_app
boot_rails
FileUtils.rm_rf("#{app_path}/config/environments")
end
def teardown
teardown_app
end
def database_url_db_name
"db/database_url_db.sqlite3"
end
def set_database_url
ENV['DATABASE_URL'] = "sqlite3://:@localhost/#{database_url_db_name}"
end
def expected
@expected ||= {}
end
def db_create_and_drop
Dir.chdir(app_path) do
output = `bundle exec rake db:create`
assert_equal output, ""
assert File.exists?(expected[:database])
assert_equal expected[:database],
ActiveRecord::Base.connection_config[:database]
output = `bundle exec rake db:drop`
assert_equal output, ""
assert !File.exists?(expected[:database])
end
end
test 'db:create and db:drop without database url' do
require "#{app_path}/config/environment"
expected[:database] = ActiveRecord::Base.configurations[Rails.env]['database']
db_create_and_drop
end
test 'db:create and db:drop with database url' do
require "#{app_path}/config/environment"
set_database_url
expected[:database] = database_url_db_name
db_create_and_drop
end
def db_migrate_and_status
Dir.chdir(app_path) do
`rails generate model book title:string`
`bundle exec rake db:migrate`
output = `bundle exec rake db:migrate:status`
assert_match(/database:\s+\S+#{expected[:database]}/, output)
assert_match(/up\s+\d{14}\s+Create books/, output)
end
end
test 'db:migrate and db:migrate:status without database_url' do
require "#{app_path}/config/environment"
expected[:database] = ActiveRecord::Base.configurations[Rails.env]['database']
db_migrate_and_status
end
test 'db:migrate and db:migrate:status with database_url' do
require "#{app_path}/config/environment"
set_database_url
expected[:database] = database_url_db_name
db_migrate_and_status
end
def db_schema_dump
Dir.chdir(app_path) do
`rails generate model book title:string`
`rake db:migrate`
`rake db:schema:dump`
schema_dump = File.read("db/schema.rb")
assert_match(/create_table \"books\"/, schema_dump)
end
end
test 'db:schema:dump without database_url' do
db_schema_dump
end
test 'db:schema:dump with database_url' do
set_database_url
db_schema_dump
end
def db_fixtures_load
Dir.chdir(app_path) do
`rails generate model book title:string`
`bundle exec rake db:migrate`
`bundle exec rake db:fixtures:load`
assert_match /#{expected[:database]}/,
ActiveRecord::Base.connection_config[:database]
require "#{app_path}/app/models/book"
assert_equal 2, Book.count
end
end
test 'db:fixtures:load without database_url' do
require "#{app_path}/config/environment"
expected[:database] = ActiveRecord::Base.configurations[Rails.env]['database']
db_fixtures_load
end
test 'db:fixtures:load with database_url' do
require "#{app_path}/config/environment"
set_database_url
expected[:database] = database_url_db_name
db_fixtures_load
end
def db_structure_dump_and_load
Dir.chdir(app_path) do
`rails generate model book title:string`
`bundle exec rake db:migrate`
`bundle exec rake db:structure:dump`
structure_dump = File.read("db/structure.sql")
assert_match(/CREATE TABLE \"books\"/, structure_dump)
`bundle exec rake db:drop`
`bundle exec rake db:structure:load`
assert_match /#{expected[:database]}/,
ActiveRecord::Base.connection_config[:database]
require "#{app_path}/app/models/book"
#if structure is not loaded correctly, exception would be raised
assert Book.count, 0
end
end
test 'db:structure:dump and db:structure:load without database_url' do
require "#{app_path}/config/environment"
expected[:database] = ActiveRecord::Base.configurations[Rails.env]['database']
db_structure_dump_and_load
end
test 'db:structure:dump and db:structure:load with database_url' do
require "#{app_path}/config/environment"
set_database_url
expected[:database] = database_url_db_name
db_structure_dump_and_load
end
def db_test_load_structure
Dir.chdir(app_path) do
`rails generate model book title:string`
`bundle exec rake db:migrate`
`bundle exec rake db:structure:dump`
`bundle exec rake db:test:load_structure`
ActiveRecord::Base.configurations = Rails.application.config.database_configuration
ActiveRecord::Base.establish_connection 'test'
require "#{app_path}/app/models/book"
#if structure is not loaded correctly, exception would be raised
assert Book.count, 0
assert_match /#{ActiveRecord::Base.configurations['test']['database']}/,
ActiveRecord::Base.connection_config[:database]
end
end
test 'db:test:load_structure without database_url' do
require "#{app_path}/config/environment"
db_test_load_structure
end
test 'db:test:load_structure with database_url' do
require "#{app_path}/config/environment"
set_database_url
db_test_load_structure
end
end
end
end