未验证 提交 e7317a25 编写于 作者: R Ryuta Kamizono 提交者: GitHub

Merge pull request #39365 from kamipo/table_options

Default engine `ENGINE=InnoDB` is no longer dumped to make schema more agnostic
* Default engine `ENGINE=InnoDB` is no longer dumped to make schema more agnostic.
Before:
```ruby
create_table "accounts", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci", force: :cascade do |t|
end
```
After:
```ruby
create_table "accounts", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
end
```
*Ryuta Kamizono*
* Added delegated type as an alternative to single-table inheritance for representing class hierarchies. * Added delegated type as an alternative to single-table inheritance for representing class hierarchies.
See ActiveRecord::DelegatedType for the full description. See ActiveRecord::DelegatedType for the full description.
*DHH* *DHH*
* Deprecate aggregations with group by duplicated fields. * Deprecate aggregations with group by duplicated fields.
......
...@@ -53,7 +53,7 @@ def visit_TableDefinition(o) ...@@ -53,7 +53,7 @@ def visit_TableDefinition(o)
end end
create_sql << "(#{statements.join(', ')})" if statements.present? create_sql << "(#{statements.join(', ')})" if statements.present?
add_table_options!(create_sql, table_options(o)) add_table_options!(create_sql, o)
create_sql << " AS #{to_sql(o.as)}" if o.as create_sql << " AS #{to_sql(o.as)}" if o.as
create_sql create_sql
end end
...@@ -106,17 +106,8 @@ def supports_index_using? ...@@ -106,17 +106,8 @@ def supports_index_using?
true true
end end
def table_options(o) def add_table_options!(create_sql, o)
table_options = {} create_sql << " #{o.options}" if o.options
table_options[:comment] = o.comment
table_options[:options] = o.options
table_options
end
def add_table_options!(create_sql, options)
if options_sql = options[:options]
create_sql << " #{options_sql}"
end
create_sql create_sql
end end
......
...@@ -277,7 +277,8 @@ def initialize( ...@@ -277,7 +277,8 @@ def initialize(
if_not_exists: false, if_not_exists: false,
options: nil, options: nil,
as: nil, as: nil,
comment: nil comment: nil,
**
) )
@conn = conn @conn = conn
@columns_hash = {} @columns_hash = {}
......
...@@ -13,9 +13,9 @@ def column_spec(column) ...@@ -13,9 +13,9 @@ def column_spec(column)
end end
def column_spec_for_primary_key(column) def column_spec_for_primary_key(column)
return {} if default_primary_key?(column) spec = {}
spec = { id: schema_type(column).inspect } spec[:id] = schema_type(column).inspect unless default_primary_key?(column)
spec.merge!(prepare_column_options(column).except!(:null, :comment)) spec.merge!(prepare_column_options(column).except!(:null))
spec[:default] ||= "nil" if explicit_primary_key_default?(column) spec[:default] ||= "nil" if explicit_primary_key_default?(column)
spec spec
end end
......
...@@ -295,13 +295,16 @@ def primary_key(table_name) ...@@ -295,13 +295,16 @@ def primary_key(table_name)
# #
# See also TableDefinition#column for details on how to create columns. # See also TableDefinition#column for details on how to create columns.
def create_table(table_name, id: :primary_key, primary_key: nil, force: nil, **options) def create_table(table_name, id: :primary_key, primary_key: nil, force: nil, **options)
td = create_table_definition( td = create_table_definition(table_name, **extract_table_options!(options))
table_name, **options.extract!(:temporary, :if_not_exists, :options, :as, :comment)
)
if id && !td.as if id && !td.as
pk = primary_key || Base.get_primary_key(table_name.to_s.singularize) pk = primary_key || Base.get_primary_key(table_name.to_s.singularize)
if id.is_a?(Hash)
options.merge!(id.except(:type))
id = id.fetch(:type, :primary_key)
end
if pk.is_a?(Array) if pk.is_a?(Array)
td.primary_keys pk td.primary_keys pk
else else
...@@ -1379,14 +1382,18 @@ def schema_creation ...@@ -1379,14 +1382,18 @@ def schema_creation
SchemaCreation.new(self) SchemaCreation.new(self)
end end
def create_table_definition(*args, **options) def create_table_definition(name, **options)
TableDefinition.new(self, *args, **options) TableDefinition.new(self, name, **options)
end end
def create_alter_table(name) def create_alter_table(name)
AlterTable.new create_table_definition(name) AlterTable.new create_table_definition(name)
end end
def extract_table_options!(options)
options.extract!(:temporary, :if_not_exists, :options, :as, :comment, :charset, :collation)
end
def fetch_type_metadata(sql_type) def fetch_type_metadata(sql_type)
cast_type = lookup_cast_type(sql_type) cast_type = lookup_cast_type(sql_type)
SqlTypeMetadata.new( SqlTypeMetadata.new(
......
...@@ -414,24 +414,31 @@ def foreign_keys(table_name) ...@@ -414,24 +414,31 @@ def foreign_keys(table_name)
end end
def table_options(table_name) # :nodoc: def table_options(table_name) # :nodoc:
table_options = {}
create_table_info = create_table_info(table_name) create_table_info = create_table_info(table_name)
# strip create_definitions and partition_options # strip create_definitions and partition_options
# Be aware that `create_table_info` might not include any table options due to `NO_TABLE_OPTIONS` sql mode. # Be aware that `create_table_info` might not include any table options due to `NO_TABLE_OPTIONS` sql mode.
raw_table_options = create_table_info.sub(/\A.*\n\) ?/m, "").sub(/\n\/\*!.*\*\/\n\z/m, "").strip raw_table_options = create_table_info.sub(/\A.*\n\) ?/m, "").sub(/\n\/\*!.*\*\/\n\z/m, "").strip
return if raw_table_options.empty?
table_options = {}
if / DEFAULT CHARSET=(?<charset>\w+)(?: COLLATE=(?<collation>\w+))?/ =~ raw_table_options
raw_table_options = $` + $' # before part + after part
table_options[:charset] = charset
table_options[:collation] = collation if collation
end
# strip AUTO_INCREMENT # strip AUTO_INCREMENT
raw_table_options.sub!(/(ENGINE=\w+)(?: AUTO_INCREMENT=\d+)/, '\1') raw_table_options.sub!(/(ENGINE=\w+)(?: AUTO_INCREMENT=\d+)/, '\1')
table_options[:options] = raw_table_options unless raw_table_options.blank?
# strip COMMENT # strip COMMENT
if raw_table_options.sub!(/ COMMENT='.+'/, "") if raw_table_options.sub!(/ COMMENT='.+'/, "")
table_options[:comment] = table_comment(table_name) table_options[:comment] = table_comment(table_name)
end end
table_options[:options] = raw_table_options unless raw_table_options == "ENGINE=InnoDB"
table_options table_options
end end
......
...@@ -40,8 +40,11 @@ def visit_IndexDefinition(o, create = false) ...@@ -40,8 +40,11 @@ def visit_IndexDefinition(o, create = false)
add_sql_comment!(sql.join(" "), o.comment) add_sql_comment!(sql.join(" "), o.comment)
end end
def add_table_options!(create_sql, options) def add_table_options!(create_sql, o)
add_sql_comment!(super, options[:comment]) create_sql = super
create_sql << " DEFAULT CHARSET=#{o.charset}" if o.charset
create_sql << " COLLATE=#{o.collation}" if o.collation
add_sql_comment!(create_sql, o.comment)
end end
def add_column_options!(sql, options) def add_column_options!(sql, options)
......
...@@ -60,6 +60,14 @@ module ColumnMethods ...@@ -60,6 +60,14 @@ module ColumnMethods
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
include ColumnMethods include ColumnMethods
attr_reader :charset, :collation
def initialize(conn, name, charset: nil, collation: nil, **)
super
@charset = charset
@collation = collation
end
def new_column_definition(name, type, **options) # :nodoc: def new_column_definition(name, type, **options) # :nodoc:
case type case type
when :virtual when :virtual
......
...@@ -154,8 +154,8 @@ def schema_creation ...@@ -154,8 +154,8 @@ def schema_creation
MySQL::SchemaCreation.new(self) MySQL::SchemaCreation.new(self)
end end
def create_table_definition(*args, **options) def create_table_definition(name, **options)
MySQL::TableDefinition.new(self, *args, **options) MySQL::TableDefinition.new(self, name, **options)
end end
def new_column_from_field(table_name, field) def new_column_from_field(table_name, field)
......
...@@ -621,8 +621,8 @@ def schema_creation ...@@ -621,8 +621,8 @@ def schema_creation
PostgreSQL::SchemaCreation.new(self) PostgreSQL::SchemaCreation.new(self)
end end
def create_table_definition(*args, **options) def create_table_definition(name, **options)
PostgreSQL::TableDefinition.new(self, *args, **options) PostgreSQL::TableDefinition.new(self, name, **options)
end end
def create_alter_table(name) def create_alter_table(name)
......
...@@ -87,8 +87,8 @@ def schema_creation ...@@ -87,8 +87,8 @@ def schema_creation
SQLite3::SchemaCreation.new(self) SQLite3::SchemaCreation.new(self)
end end
def create_table_definition(*args, **options) def create_table_definition(name, **options)
SQLite3::TableDefinition.new(self, *args, **options) SQLite3::TableDefinition.new(self, name, **options)
end end
def validate_index_length!(table_name, new_name, internal = false) def validate_index_length!(table_name, new_name, internal = false)
......
...@@ -125,7 +125,10 @@ def table(table, stream) ...@@ -125,7 +125,10 @@ def table(table, stream)
tbl.print ", primary_key: #{pk.inspect}" unless pk == "id" tbl.print ", primary_key: #{pk.inspect}" unless pk == "id"
pkcol = columns.detect { |c| c.name == pk } pkcol = columns.detect { |c| c.name == pk }
pkcolspec = column_spec_for_primary_key(pkcol) pkcolspec = column_spec_for_primary_key(pkcol)
if pkcolspec.present? unless pkcolspec.empty?
if pkcolspec != pkcolspec.slice(:id, :default)
pkcolspec = { id: { type: pkcolspec.delete(:id), **pkcolspec }.compact }
end
tbl.print ", #{format_colspec(pkcolspec)}" tbl.print ", #{format_colspec(pkcolspec)}"
end end
when Array when Array
...@@ -240,7 +243,9 @@ def foreign_keys(table, stream) ...@@ -240,7 +243,9 @@ def foreign_keys(table, stream)
end end
def format_colspec(colspec) def format_colspec(colspec)
colspec.map { |key, value| "#{key}: #{value}" }.join(", ") colspec.map do |key, value|
"#{key}: #{ value.is_a?(Hash) ? "{ #{format_colspec(value)} }" : value }"
end.join(", ")
end end
def format_options(options) def format_options(options)
......
...@@ -9,7 +9,7 @@ class Mysql2CharsetCollationTest < ActiveRecord::Mysql2TestCase ...@@ -9,7 +9,7 @@ class Mysql2CharsetCollationTest < ActiveRecord::Mysql2TestCase
setup do setup do
@connection = ActiveRecord::Base.connection @connection = ActiveRecord::Base.connection
@connection.create_table :charset_collations, force: true do |t| @connection.create_table :charset_collations, id: { type: :string, collation: "utf8mb4_bin" }, force: true do |t|
t.string :string_ascii_bin, charset: "ascii", collation: "ascii_bin" t.string :string_ascii_bin, charset: "ascii", collation: "ascii_bin"
t.text :text_ucs2_unicode_ci, charset: "ucs2", collation: "ucs2_unicode_ci" t.text :text_ucs2_unicode_ci, charset: "ucs2", collation: "ucs2_unicode_ci"
end end
...@@ -50,6 +50,7 @@ class Mysql2CharsetCollationTest < ActiveRecord::Mysql2TestCase ...@@ -50,6 +50,7 @@ class Mysql2CharsetCollationTest < ActiveRecord::Mysql2TestCase
test "schema dump includes collation" do test "schema dump includes collation" do
output = dump_table_schema("charset_collations") output = dump_table_schema("charset_collations")
assert_match %r/create_table "charset_collations", id: { type: :string, collation: "utf8mb4_bin" }/, output
assert_match %r{t\.string\s+"string_ascii_bin",\s+collation: "ascii_bin"$}, output assert_match %r{t\.string\s+"string_ascii_bin",\s+collation: "ascii_bin"$}, output
assert_match %r{t\.text\s+"text_ucs2_unicode_ci",\s+collation: "ucs2_unicode_ci"$}, output assert_match %r{t\.text\s+"text_ucs2_unicode_ci",\s+collation: "ucs2_unicode_ci"$}, output
end end
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
class Mysql2TableOptionsTest < ActiveRecord::Mysql2TestCase class Mysql2TableOptionsTest < ActiveRecord::Mysql2TestCase
include SchemaDumpingHelper include SchemaDumpingHelper
self.use_transactional_tests = false
def setup def setup
@connection = ActiveRecord::Base.connection @connection = ActiveRecord::Base.connection
...@@ -17,29 +18,36 @@ def teardown ...@@ -17,29 +18,36 @@ def teardown
test "table options with ENGINE" do test "table options with ENGINE" do
@connection.create_table "mysql_table_options", force: true, options: "ENGINE=MyISAM" @connection.create_table "mysql_table_options", force: true, options: "ENGINE=MyISAM"
output = dump_table_schema("mysql_table_options") output = dump_table_schema("mysql_table_options")
options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options] expected = /create_table "mysql_table_options", charset: "utf8mb4"(?:, collation: "\w+")?, options: "ENGINE=MyISAM", force: :cascade/
assert_match %r{ENGINE=MyISAM}, options assert_match expected, output
end end
test "table options with ROW_FORMAT" do test "table options with ROW_FORMAT" do
@connection.create_table "mysql_table_options", force: true, options: "ROW_FORMAT=REDUNDANT" @connection.create_table "mysql_table_options", force: true, options: "ROW_FORMAT=REDUNDANT"
output = dump_table_schema("mysql_table_options") output = dump_table_schema("mysql_table_options")
options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options] expected = /create_table "mysql_table_options", charset: "utf8mb4"(?:, collation: "\w+")?, options: "ENGINE=InnoDB ROW_FORMAT=REDUNDANT", force: :cascade/
assert_match %r{ROW_FORMAT=REDUNDANT}, options assert_match expected, output
end end
test "table options with CHARSET" do test "table options with CHARSET" do
@connection.create_table "mysql_table_options", force: true, options: "CHARSET=utf8mb4" @connection.create_table "mysql_table_options", force: true, options: "CHARSET=latin1"
output = dump_table_schema("mysql_table_options") output = dump_table_schema("mysql_table_options")
options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options] expected = /create_table "mysql_table_options", charset: "latin1", force: :cascade/
assert_match %r{CHARSET=utf8mb4}, options assert_match expected, output
end end
test "table options with COLLATE" do test "table options with COLLATE" do
@connection.create_table "mysql_table_options", force: true, options: "COLLATE=utf8mb4_bin" @connection.create_table "mysql_table_options", force: true, options: "COLLATE=utf8mb4_bin"
output = dump_table_schema("mysql_table_options") output = dump_table_schema("mysql_table_options")
options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options] expected = /create_table "mysql_table_options", charset: "utf8mb4", collation: "utf8mb4_bin", force: :cascade/
assert_match %r{COLLATE=utf8mb4_bin}, options assert_match expected, output
end
test "charset and collation options" do
@connection.create_table "mysql_table_options", force: true, charset: "utf8mb4", collation: "utf8mb4_bin"
output = dump_table_schema("mysql_table_options")
expected = /create_table "mysql_table_options", charset: "utf8mb4", collation: "utf8mb4_bin", force: :cascade/
assert_match expected, output
end end
test "schema dump works with NO_TABLE_OPTIONS sql mode" do test "schema dump works with NO_TABLE_OPTIONS sql mode" do
...@@ -60,47 +68,10 @@ def teardown ...@@ -60,47 +68,10 @@ def teardown
end end
end end
class Mysql2DefaultEngineOptionSchemaDumpTest < ActiveRecord::Mysql2TestCase class Mysql2DefaultEngineOptionTest < ActiveRecord::Mysql2TestCase
include SchemaDumpingHelper include SchemaDumpingHelper
self.use_transactional_tests = false self.use_transactional_tests = false
def setup
@verbose_was = ActiveRecord::Migration.verbose
ActiveRecord::Migration.verbose = false
end
def teardown
ActiveRecord::Base.connection.drop_table "mysql_table_options", if_exists: true
ActiveRecord::Migration.verbose = @verbose_was
ActiveRecord::SchemaMigration.delete_all rescue nil
end
test "schema dump includes ENGINE=InnoDB if not provided" do
ActiveRecord::Base.connection.create_table "mysql_table_options", force: true
output = dump_table_schema("mysql_table_options")
options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options]
assert_match %r{ENGINE=InnoDB}, options
end
test "schema dump includes ENGINE=InnoDB in legacy migrations" do
migration = Class.new(ActiveRecord::Migration[5.1]) do
def migrate(x)
create_table "mysql_table_options", force: true
end
end.new
ActiveRecord::Migrator.new(:up, [migration], ActiveRecord::Base.connection.schema_migration).migrate
output = dump_table_schema("mysql_table_options")
options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options]
assert_match %r{ENGINE=InnoDB}, options
end
end
class Mysql2DefaultEngineOptionSqlOutputTest < ActiveRecord::Mysql2TestCase
self.use_transactional_tests = false
def setup def setup
@logger_was = ActiveRecord::Base.logger @logger_was = ActiveRecord::Base.logger
@log = StringIO.new @log = StringIO.new
...@@ -120,6 +91,10 @@ def teardown ...@@ -120,6 +91,10 @@ def teardown
ActiveRecord::Base.connection.create_table "mysql_table_options", force: true ActiveRecord::Base.connection.create_table "mysql_table_options", force: true
assert_no_match %r{ENGINE=InnoDB}, @log.string assert_no_match %r{ENGINE=InnoDB}, @log.string
output = dump_table_schema("mysql_table_options")
expected = /create_table "mysql_table_options", charset: "utf8mb4"(?:, collation: "\w+")?, force: :cascade/
assert_match expected, output
end end
test "legacy migrations contain default ENGINE=InnoDB option" do test "legacy migrations contain default ENGINE=InnoDB option" do
...@@ -132,5 +107,9 @@ def migrate(x) ...@@ -132,5 +107,9 @@ def migrate(x)
ActiveRecord::Migrator.new(:up, [migration], ActiveRecord::Base.connection.schema_migration).migrate ActiveRecord::Migrator.new(:up, [migration], ActiveRecord::Base.connection.schema_migration).migrate
assert_match %r{ENGINE=InnoDB}, @log.string assert_match %r{ENGINE=InnoDB}, @log.string
output = dump_table_schema("mysql_table_options")
expected = /create_table "mysql_table_options", charset: "utf8mb4"(?:, collation: "\w+")?, force: :cascade/
assert_match expected, output
end end
end end
...@@ -39,7 +39,7 @@ class PkCommented < ActiveRecord::Base ...@@ -39,7 +39,7 @@ class PkCommented < ActiveRecord::Base
end end
@connection.create_table("pk_commenteds", comment: "Table comment", id: false, force: true) do |t| @connection.create_table("pk_commenteds", comment: "Table comment", id: false, force: true) do |t|
t.integer :id, comment: "Primary key comment", primary_key: true t.primary_key :id, comment: "Primary key comment"
end end
Commented.reset_column_information Commented.reset_column_information
...@@ -197,8 +197,7 @@ def test_comment_on_primary_key ...@@ -197,8 +197,7 @@ def test_comment_on_primary_key
def test_schema_dump_with_primary_key_comment def test_schema_dump_with_primary_key_comment
output = dump_table_schema "pk_commenteds" output = dump_table_schema "pk_commenteds"
assert_match %r[create_table "pk_commenteds",.*\s+comment: "Table comment"], output assert_match %r[create_table "pk_commenteds", id: { comment: "Primary key comment" }.*, comment: "Table comment"], output
assert_no_match %r[create_table "pk_commenteds",.*\s+comment: "Primary key comment"], output
end end
end end
end end
...@@ -312,7 +312,7 @@ def test_any_type_primary_key ...@@ -312,7 +312,7 @@ def test_any_type_primary_key
test "schema dump primary key includes type and options" do test "schema dump primary key includes type and options" do
schema = dump_table_schema "barcodes" schema = dump_table_schema "barcodes"
assert_match %r{create_table "barcodes", primary_key: "code", id: :string, limit: 42}, schema assert_match %r/create_table "barcodes", primary_key: "code", id: { type: :string, limit: 42 }/, schema
assert_no_match %r{t\.index \["code"\]}, schema assert_no_match %r{t\.index \["code"\]}, schema
end end
...@@ -320,7 +320,7 @@ def test_any_type_primary_key ...@@ -320,7 +320,7 @@ def test_any_type_primary_key
test "schema typed primary key column" do test "schema typed primary key column" do
@connection.create_table(:scheduled_logs, id: :timestamp, precision: 6, force: true) @connection.create_table(:scheduled_logs, id: :timestamp, precision: 6, force: true)
schema = dump_table_schema("scheduled_logs") schema = dump_table_schema("scheduled_logs")
assert_match %r/create_table "scheduled_logs", id: :timestamp, precision: 6/, schema assert_match %r/create_table "scheduled_logs", id: { type: :timestamp, precision: 6.* }/, schema
end end
end end
end end
...@@ -462,7 +462,7 @@ class Widget < ActiveRecord::Base ...@@ -462,7 +462,7 @@ class Widget < ActiveRecord::Base
assert_predicate column, :unsigned? assert_predicate column, :unsigned?
schema = dump_table_schema "widgets" schema = dump_table_schema "widgets"
assert_match %r{create_table "widgets", id: :integer, unsigned: true, }, schema assert_match %r/create_table "widgets", id: { type: :integer, unsigned: true }/, schema
end end
test "bigint primary key with unsigned" do test "bigint primary key with unsigned" do
...@@ -474,7 +474,7 @@ class Widget < ActiveRecord::Base ...@@ -474,7 +474,7 @@ class Widget < ActiveRecord::Base
assert_predicate column, :unsigned? assert_predicate column, :unsigned?
schema = dump_table_schema "widgets" schema = dump_table_schema "widgets"
assert_match %r{create_table "widgets", id: :bigint, unsigned: true, }, schema assert_match %r/create_table "widgets", id: { type: :bigint, unsigned: true }/, schema
end end
end end
end end
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册