mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Merge pull request #34832 from gmcgibbon/db_system_change_command
Add rails db:system:change command
This commit is contained in:
commit
90536ebfb3
12 changed files with 378 additions and 89 deletions
|
@ -1,7 +1,20 @@
|
|||
* Add `rails db:system:change` command for changing databases.
|
||||
|
||||
```
|
||||
bin/rails db:system:change --to=postgresql
|
||||
force config/database.yml
|
||||
gsub Gemfile
|
||||
```
|
||||
|
||||
The change command copies a template `config/database.yml` with the target database adapter into your app, and replaces your database gem with the target database gem.
|
||||
|
||||
*Gannon McGibbon*
|
||||
|
||||
* Add `rails test:channels`.
|
||||
|
||||
*bogdanvlviv*
|
||||
|
||||
|
||||
* Use original `bundler` environment variables during the process of generating a new rails project.
|
||||
|
||||
*Marco Costa*
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "rails/generators"
|
||||
require "rails/generators/rails/db/system/change/change_generator"
|
||||
|
||||
module Rails
|
||||
module Command
|
||||
module Db
|
||||
module System
|
||||
class ChangeCommand < Base # :nodoc:
|
||||
class_option :to, desc: "The database system to switch to."
|
||||
|
||||
def perform
|
||||
Rails::Generators::Db::System::ChangeGenerator.start
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -23,6 +23,8 @@ module Rails
|
|||
autoload :ActiveModel, "rails/generators/active_model"
|
||||
autoload :Base, "rails/generators/base"
|
||||
autoload :Migration, "rails/generators/migration"
|
||||
autoload :Database, "rails/generators/database"
|
||||
autoload :AppName, "rails/generators/app_name"
|
||||
autoload :NamedBase, "rails/generators/named_base"
|
||||
autoload :ResourceHelpers, "rails/generators/resource_helpers"
|
||||
autoload :TestCase, "rails/generators/test_case"
|
||||
|
@ -218,6 +220,7 @@ module Rails
|
|||
rails.delete("encryption_key_file")
|
||||
rails.delete("master_key")
|
||||
rails.delete("credentials")
|
||||
rails.delete("db:system:change")
|
||||
|
||||
hidden_namespaces.each { |n| groups.delete(n.to_s) }
|
||||
|
||||
|
|
|
@ -11,9 +11,8 @@ require "active_support/core_ext/array/extract_options"
|
|||
module Rails
|
||||
module Generators
|
||||
class AppBase < Base # :nodoc:
|
||||
DATABASES = %w( mysql postgresql sqlite3 oracle frontbase ibm_db sqlserver )
|
||||
JDBC_DATABASES = %w( jdbcmysql jdbcsqlite3 jdbcpostgresql jdbc )
|
||||
DATABASES.concat(JDBC_DATABASES)
|
||||
include Database
|
||||
include AppName
|
||||
|
||||
attr_accessor :rails_template
|
||||
add_shebang_option!
|
||||
|
@ -106,7 +105,6 @@ module Rails
|
|||
@gem_filter = lambda { |gem| true }
|
||||
@extra_entries = []
|
||||
super
|
||||
convert_database_option_for_jruby
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -306,34 +304,6 @@ module Rails
|
|||
end
|
||||
end
|
||||
|
||||
def gem_for_database
|
||||
# %w( mysql postgresql sqlite3 oracle frontbase ibm_db sqlserver jdbcmysql jdbcsqlite3 jdbcpostgresql )
|
||||
case options[:database]
|
||||
when "mysql" then ["mysql2", [">= 0.4.4"]]
|
||||
when "postgresql" then ["pg", [">= 0.18", "< 2.0"]]
|
||||
when "oracle" then ["activerecord-oracle_enhanced-adapter", nil]
|
||||
when "frontbase" then ["ruby-frontbase", nil]
|
||||
when "sqlserver" then ["activerecord-sqlserver-adapter", nil]
|
||||
when "jdbcmysql" then ["activerecord-jdbcmysql-adapter", nil]
|
||||
when "jdbcsqlite3" then ["activerecord-jdbcsqlite3-adapter", nil]
|
||||
when "jdbcpostgresql" then ["activerecord-jdbcpostgresql-adapter", nil]
|
||||
when "jdbc" then ["activerecord-jdbc-adapter", nil]
|
||||
else [options[:database], nil]
|
||||
end
|
||||
end
|
||||
|
||||
def convert_database_option_for_jruby
|
||||
if defined?(JRUBY_VERSION)
|
||||
opt = options.dup
|
||||
case opt[:database]
|
||||
when "postgresql" then opt[:database] = "jdbcpostgresql"
|
||||
when "mysql" then opt[:database] = "jdbcmysql"
|
||||
when "sqlite3" then opt[:database] = "jdbcsqlite3"
|
||||
end
|
||||
self.options = opt.freeze
|
||||
end
|
||||
end
|
||||
|
||||
def assets_gemfile_entry
|
||||
return [] if options[:skip_sprockets]
|
||||
|
||||
|
|
50
railties/lib/rails/generators/app_name.rb
Normal file
50
railties/lib/rails/generators/app_name.rb
Normal file
|
@ -0,0 +1,50 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Rails
|
||||
module Generators
|
||||
module AppName # :nodoc:
|
||||
RESERVED_NAMES = %w(application destroy plugin runner test)
|
||||
|
||||
private
|
||||
def app_name
|
||||
@app_name ||= original_app_name.tr("-", "_")
|
||||
end
|
||||
|
||||
def original_app_name
|
||||
@original_app_name ||= (defined_app_const_base? ? defined_app_name : File.basename(destination_root)).tr('\\', "").tr(". ", "_")
|
||||
end
|
||||
|
||||
def defined_app_name
|
||||
defined_app_const_base.underscore
|
||||
end
|
||||
|
||||
def defined_app_const_base
|
||||
Rails.respond_to?(:application) && defined?(Rails::Application) &&
|
||||
Rails.application.is_a?(Rails::Application) && Rails.application.class.name.chomp("::Application")
|
||||
end
|
||||
|
||||
alias :defined_app_const_base? :defined_app_const_base
|
||||
|
||||
def app_const_base
|
||||
@app_const_base ||= defined_app_const_base || app_name.gsub(/\W/, "_").squeeze("_").camelize
|
||||
end
|
||||
alias :camelized :app_const_base
|
||||
|
||||
def app_const
|
||||
@app_const ||= "#{app_const_base}::Application"
|
||||
end
|
||||
|
||||
def valid_const?
|
||||
if /^\d/.match?(app_const)
|
||||
raise Error, "Invalid application name #{original_app_name}. Please give a name which does not start with numbers."
|
||||
elsif RESERVED_NAMES.include?(original_app_name)
|
||||
raise Error, "Invalid application name #{original_app_name}. Please give a " \
|
||||
"name which does not match one of the reserved rails " \
|
||||
"words: #{RESERVED_NAMES.join(", ")}"
|
||||
elsif Object.const_defined?(app_const_base)
|
||||
raise Error, "Invalid application name #{original_app_name}, constant #{app_const_base} is already in use. Please choose another application name."
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
57
railties/lib/rails/generators/database.rb
Normal file
57
railties/lib/rails/generators/database.rb
Normal file
|
@ -0,0 +1,57 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Rails
|
||||
module Generators
|
||||
module Database # :nodoc:
|
||||
JDBC_DATABASES = %w( jdbcmysql jdbcsqlite3 jdbcpostgresql jdbc )
|
||||
DATABASES = %w( mysql postgresql sqlite3 oracle frontbase ibm_db sqlserver ) + JDBC_DATABASES
|
||||
|
||||
def initialize(*)
|
||||
super
|
||||
convert_database_option_for_jruby
|
||||
end
|
||||
|
||||
def gem_for_database(database = options[:database])
|
||||
case database
|
||||
when "mysql" then ["mysql2", [">= 0.4.4"]]
|
||||
when "postgresql" then ["pg", [">= 0.18", "< 2.0"]]
|
||||
when "oracle" then ["activerecord-oracle_enhanced-adapter", nil]
|
||||
when "frontbase" then ["ruby-frontbase", nil]
|
||||
when "sqlserver" then ["activerecord-sqlserver-adapter", nil]
|
||||
when "jdbcmysql" then ["activerecord-jdbcmysql-adapter", nil]
|
||||
when "jdbcsqlite3" then ["activerecord-jdbcsqlite3-adapter", nil]
|
||||
when "jdbcpostgresql" then ["activerecord-jdbcpostgresql-adapter", nil]
|
||||
when "jdbc" then ["activerecord-jdbc-adapter", nil]
|
||||
else [database, nil]
|
||||
end
|
||||
end
|
||||
|
||||
def convert_database_option_for_jruby
|
||||
if defined?(JRUBY_VERSION)
|
||||
opt = options.dup
|
||||
case opt[:database]
|
||||
when "postgresql" then opt[:database] = "jdbcpostgresql"
|
||||
when "mysql" then opt[:database] = "jdbcmysql"
|
||||
when "sqlite3" then opt[:database] = "jdbcsqlite3"
|
||||
end
|
||||
self.options = opt.freeze
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def mysql_socket
|
||||
@mysql_socket ||= [
|
||||
"/tmp/mysql.sock", # default
|
||||
"/var/run/mysqld/mysqld.sock", # debian/gentoo
|
||||
"/var/tmp/mysql.sock", # freebsd
|
||||
"/var/lib/mysql/mysql.sock", # fedora
|
||||
"/opt/local/lib/mysql/mysql.sock", # fedora
|
||||
"/opt/local/var/run/mysqld/mysqld.sock", # mac + darwinports + mysql
|
||||
"/opt/local/var/run/mysql4/mysqld.sock", # mac + darwinports + mysql4
|
||||
"/opt/local/var/run/mysql5/mysqld.sock", # mac + darwinports + mysql5
|
||||
"/opt/lampp/var/mysql/mysql.sock" # xampp for linux
|
||||
].find { |f| File.exist?(f) } unless Gem.win_platform?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -242,7 +242,6 @@ module Rails
|
|||
# We need to store the RAILS_DEV_PATH in a constant, otherwise the path
|
||||
# can change in Ruby 1.8.7 when we FileUtils.cd.
|
||||
RAILS_DEV_PATH = File.expand_path("../../../../../..", __dir__)
|
||||
RESERVED_NAMES = %w[application destroy plugin runner test]
|
||||
|
||||
class AppGenerator < AppBase # :nodoc:
|
||||
WEBPACKS = %w( react vue angular elm stimulus )
|
||||
|
@ -269,7 +268,7 @@ module Rails
|
|||
super
|
||||
|
||||
if !options[:skip_active_record] && !DATABASES.include?(options[:database])
|
||||
raise Error, "Invalid value for --database option. Supported for preconfiguration are: #{DATABASES.join(", ")}."
|
||||
raise Error, "Invalid value for --database option. Supported preconfigurations are: #{DATABASES.join(", ")}."
|
||||
end
|
||||
|
||||
# Force sprockets and yarn to be skipped when generating API only apps.
|
||||
|
@ -491,60 +490,6 @@ module Rails
|
|||
create_file(*args, &block)
|
||||
end
|
||||
|
||||
def app_name
|
||||
@app_name ||= original_app_name.tr("-", "_")
|
||||
end
|
||||
|
||||
def original_app_name
|
||||
@original_app_name ||= (defined_app_const_base? ? defined_app_name : File.basename(destination_root)).tr('\\', "").tr(". ", "_")
|
||||
end
|
||||
|
||||
def defined_app_name
|
||||
defined_app_const_base.underscore
|
||||
end
|
||||
|
||||
def defined_app_const_base
|
||||
Rails.respond_to?(:application) && defined?(Rails::Application) &&
|
||||
Rails.application.is_a?(Rails::Application) && Rails.application.class.name.sub(/::Application$/, "")
|
||||
end
|
||||
|
||||
alias :defined_app_const_base? :defined_app_const_base
|
||||
|
||||
def app_const_base
|
||||
@app_const_base ||= defined_app_const_base || app_name.gsub(/\W/, "_").squeeze("_").camelize
|
||||
end
|
||||
alias :camelized :app_const_base
|
||||
|
||||
def app_const
|
||||
@app_const ||= "#{app_const_base}::Application"
|
||||
end
|
||||
|
||||
def valid_const?
|
||||
if /^\d/.match?(app_const)
|
||||
raise Error, "Invalid application name #{original_app_name}. Please give a name which does not start with numbers."
|
||||
elsif RESERVED_NAMES.include?(original_app_name)
|
||||
raise Error, "Invalid application name #{original_app_name}. Please give a " \
|
||||
"name which does not match one of the reserved rails " \
|
||||
"words: #{RESERVED_NAMES.join(", ")}"
|
||||
elsif Object.const_defined?(app_const_base)
|
||||
raise Error, "Invalid application name #{original_app_name}, constant #{app_const_base} is already in use. Please choose another application name."
|
||||
end
|
||||
end
|
||||
|
||||
def mysql_socket
|
||||
@mysql_socket ||= [
|
||||
"/tmp/mysql.sock", # default
|
||||
"/var/run/mysqld/mysqld.sock", # debian/gentoo
|
||||
"/var/tmp/mysql.sock", # freebsd
|
||||
"/var/lib/mysql/mysql.sock", # fedora
|
||||
"/opt/local/lib/mysql/mysql.sock", # fedora
|
||||
"/opt/local/var/run/mysqld/mysqld.sock", # mac + darwinports + mysql
|
||||
"/opt/local/var/run/mysql4/mysqld.sock", # mac + darwinports + mysql4
|
||||
"/opt/local/var/run/mysql5/mysqld.sock", # mac + darwinports + mysql5
|
||||
"/opt/lampp/var/mysql/mysql.sock" # xampp for linux
|
||||
].find { |f| File.exist?(f) } unless Gem.win_platform?
|
||||
end
|
||||
|
||||
def get_builder_class
|
||||
defined?(::AppBuilder) ? ::AppBuilder : Rails::AppBuilder
|
||||
end
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "rails/generators/base"
|
||||
|
||||
module Rails
|
||||
module Generators
|
||||
module Db
|
||||
module System
|
||||
class ChangeGenerator < Base # :nodoc:
|
||||
include Database
|
||||
include AppName
|
||||
|
||||
class_option :to, required: true,
|
||||
desc: "The database system to switch to."
|
||||
|
||||
def self.default_generator_root
|
||||
path = File.expand_path(File.join(base_name, "app"), base_root)
|
||||
path if File.exist?(path)
|
||||
end
|
||||
|
||||
def initialize(*)
|
||||
super
|
||||
|
||||
unless DATABASES.include?(options[:to])
|
||||
raise Error, "Invalid value for --to option. Supported preconfigurations are: #{DATABASES.join(", ")}."
|
||||
end
|
||||
|
||||
opt = options.dup
|
||||
opt[:database] ||= opt[:to]
|
||||
self.options = opt.freeze
|
||||
end
|
||||
|
||||
def edit_database_config
|
||||
template("config/databases/#{options[:database]}.yml", "config/database.yml")
|
||||
end
|
||||
|
||||
def edit_gemfile
|
||||
database_gem_name, _ = gem_for_database
|
||||
gsub_file("Gemfile", all_database_gems_regex, database_gem_name)
|
||||
end
|
||||
|
||||
private
|
||||
def all_database_gems
|
||||
DATABASES.map { |database| gem_for_database(database) }
|
||||
end
|
||||
|
||||
def all_database_gems_regex
|
||||
all_database_gem_names = all_database_gems.map(&:first)
|
||||
/(\b#{all_database_gem_names.join('\b|\b')}\b)/
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
62
railties/test/commands/db_system_change_test.rb
Normal file
62
railties/test/commands/db_system_change_test.rb
Normal file
|
@ -0,0 +1,62 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "isolation/abstract_unit"
|
||||
require "rails/command"
|
||||
require "rails/commands/db/system/change/change_command"
|
||||
|
||||
class Rails::Command::Db::System::ChangeCommandTest < ActiveSupport::TestCase
|
||||
include ActiveSupport::Testing::Isolation
|
||||
|
||||
setup { build_app }
|
||||
|
||||
teardown { teardown_app }
|
||||
|
||||
test "change to existing database" do
|
||||
change_database(to: "sqlite3")
|
||||
|
||||
output = change_database(to: "sqlite3")
|
||||
|
||||
assert_match "identical config/database.yml", output
|
||||
assert_match "gsub Gemfile", output
|
||||
end
|
||||
|
||||
test "change to invalid database" do
|
||||
output = change_database(to: "invalid-db")
|
||||
|
||||
assert_match <<~MSG.squish, output
|
||||
Invalid value for --to option.
|
||||
Supported preconfigurations are:
|
||||
mysql, postgresql, sqlite3, oracle, frontbase,
|
||||
ibm_db, sqlserver, jdbcmysql, jdbcsqlite3,
|
||||
jdbcpostgresql, jdbc.
|
||||
MSG
|
||||
end
|
||||
|
||||
test "change to postgresql" do
|
||||
output = change_database(to: "postgresql")
|
||||
|
||||
assert_match "force config/database.yml", output
|
||||
assert_match "gsub Gemfile", output
|
||||
end
|
||||
|
||||
test "change to mysql" do
|
||||
output = change_database(to: "mysql")
|
||||
|
||||
assert_match "force config/database.yml", output
|
||||
assert_match "gsub Gemfile", output
|
||||
end
|
||||
|
||||
test "change to sqlite3" do
|
||||
change_database(to: "postgresql")
|
||||
output = change_database(to: "sqlite3")
|
||||
|
||||
assert_match "force config/database.yml", output
|
||||
assert_match "gsub Gemfile", output
|
||||
end
|
||||
|
||||
private
|
||||
def change_database(to:, **options)
|
||||
args = ["--to", to]
|
||||
rails "db:system:change", args, **options
|
||||
end
|
||||
end
|
78
railties/test/generators/db_system_change_generator_test.rb
Normal file
78
railties/test/generators/db_system_change_generator_test.rb
Normal file
|
@ -0,0 +1,78 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "generators/generators_test_helper"
|
||||
require "rails/generators/rails/db/system/change/change_generator"
|
||||
|
||||
module Rails
|
||||
module Generators
|
||||
module Db
|
||||
module System
|
||||
class ChangeGeneratorTest < Rails::Generators::TestCase
|
||||
include GeneratorsTestHelper
|
||||
|
||||
setup do
|
||||
copy_gemfile(
|
||||
GemfileEntry.new("sqlite3", nil, "Use sqlite3 as the database for Active Record")
|
||||
)
|
||||
end
|
||||
|
||||
test "change to invalid database" do
|
||||
output = capture(:stderr) do
|
||||
run_generator ["--to", "invalid-db"]
|
||||
end
|
||||
|
||||
assert_match <<~MSG.squish, output
|
||||
Invalid value for --to option.
|
||||
Supported preconfigurations are:
|
||||
mysql, postgresql, sqlite3, oracle, frontbase,
|
||||
ibm_db, sqlserver, jdbcmysql, jdbcsqlite3,
|
||||
jdbcpostgresql, jdbc.
|
||||
MSG
|
||||
end
|
||||
|
||||
test "change to postgresql" do
|
||||
run_generator ["--to", "postgresql"]
|
||||
|
||||
assert_file("config/database.yml") do |content|
|
||||
assert_match "adapter: postgresql", content
|
||||
assert_match "database: test_app", content
|
||||
end
|
||||
|
||||
assert_file("Gemfile") do |content|
|
||||
assert_match "# Use pg as the database for Active Record", content
|
||||
assert_match "gem 'pg'", content
|
||||
end
|
||||
end
|
||||
|
||||
test "change to mysql" do
|
||||
run_generator ["--to", "mysql"]
|
||||
|
||||
assert_file("config/database.yml") do |content|
|
||||
assert_match "adapter: mysql2", content
|
||||
assert_match "database: test_app", content
|
||||
end
|
||||
|
||||
assert_file("Gemfile") do |content|
|
||||
assert_match "# Use mysql2 as the database for Active Record", content
|
||||
assert_match "gem 'mysql2'", content
|
||||
end
|
||||
end
|
||||
|
||||
test "change to sqlite3" do
|
||||
run_generator ["--to", "sqlite3"]
|
||||
|
||||
assert_file("config/database.yml") do |content|
|
||||
assert_match "adapter: sqlite3", content
|
||||
assert_match "db/development.sqlite3", content
|
||||
end
|
||||
|
||||
assert_file("Gemfile") do |content|
|
||||
assert_match "# Use sqlite3 as the database for Active Record", content
|
||||
assert_match "gem 'sqlite3'", content
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -30,6 +30,12 @@ module GeneratorsTestHelper
|
|||
include ActiveSupport::Testing::Stream
|
||||
include ActiveSupport::Testing::MethodCallAssertions
|
||||
|
||||
GemfileEntry = Struct.new(:name, :version, :comment, :options, :commented_out) do
|
||||
def initialize(name, version, comment, options = {}, commented_out = false)
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def self.included(base)
|
||||
base.class_eval do
|
||||
destination File.join(Rails.root, "tmp")
|
||||
|
@ -63,4 +69,34 @@ module GeneratorsTestHelper
|
|||
FileUtils.mkdir_p(destination)
|
||||
FileUtils.cp routes, File.join(destination, "routes.rb")
|
||||
end
|
||||
|
||||
def copy_gemfile(*gemfile_entries)
|
||||
locals = gemfile_locals.merge(gemfile_entries: gemfile_entries)
|
||||
gemfile = File.expand_path("../../lib/rails/generators/rails/app/templates/Gemfile.tt", __dir__)
|
||||
gemfile = evaluate_template(gemfile, locals)
|
||||
destination = File.join(destination_root)
|
||||
File.write File.join(destination, "Gemfile"), gemfile
|
||||
end
|
||||
|
||||
def evaluate_template(file, locals = {})
|
||||
erb = ERB.new(File.read(file), nil, "-", "@output_buffer")
|
||||
context = Class.new do
|
||||
locals.each do |local, value|
|
||||
class_attribute local, default: value
|
||||
end
|
||||
end
|
||||
erb.result(context.new.instance_eval("binding"))
|
||||
end
|
||||
|
||||
private
|
||||
def gemfile_locals
|
||||
{
|
||||
skip_active_storage: true,
|
||||
depend_on_bootsnap: false,
|
||||
depend_on_listen: false,
|
||||
spring_install: false,
|
||||
depends_on_system_test: false,
|
||||
options: ActiveSupport::OrderedOptions.new,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -476,7 +476,7 @@ Module.new do
|
|||
`yarn build`
|
||||
end
|
||||
|
||||
`#{Gem.ruby} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails new #{app_template_path} --skip-gemfile --skip-listen --no-rc`
|
||||
`#{Gem.ruby} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails new #{app_template_path} --skip-bundle --skip-listen --no-rc`
|
||||
File.open("#{app_template_path}/config/boot.rb", "w") do |f|
|
||||
f.puts "require 'rails/all'"
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue