未验证 提交 90536ebf 编写于 作者: K Kasper Timm Hansen 提交者: GitHub

Merge pull request #34832 from gmcgibbon/db_system_change_command

Add rails db:system:change command
* 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*
......
# 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 Generators
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 @@ def sorted_groups
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 @@
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 @@ def initialize(*args)
@gem_filter = lambda { |gem| true }
@extra_entries = []
super
convert_database_option_for_jruby
end
private
......@@ -306,34 +304,6 @@ def rails_version_specifier(gem_version = Rails.gem_version)
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]
......
# 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
# 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 Generators
# 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 @@ def initialize(*args)
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 @@ def file(*args, &block)
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
......
# 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
# 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
# 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 @@ def copy_routes
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 @@ class ActiveSupport::TestCase
`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
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册