Allow changing text and blob size without giving the limit option

In MySQL, the text column size is 65,535 bytes by default (1 GiB in
PostgreSQL). It is sometimes too short when people want to use a text
column, so they sometimes change the text size to mediumtext (16 MiB) or
longtext (4 GiB) by giving the `limit` option.

Unlike MySQL, PostgreSQL doesn't allow the `limit` option for a text
column (raises ERROR: type modifier is not allowed for type "text").
So `limit: 4294967295` (longtext) couldn't be used in Action Text.

I've allowed changing text and blob size without giving the `limit`
option, it prevents that migration failure on PostgreSQL.
This commit is contained in:
Ryuta Kamizono 2019-01-25 22:01:07 +09:00
parent b8baa15adb
commit 1745e905a3
11 changed files with 65 additions and 23 deletions

@ -5,11 +5,7 @@ def change
t.string :message_id, null: false
t.string :message_checksum, null: false
if supports_datetime_with_precision?
t.timestamps precision: 6
else
t.timestamps
end
t.timestamps
t.index [ :message_id, :message_checksum ], name: "index_action_mailbox_inbound_emails_uniqueness", unique: true
end

@ -2,7 +2,7 @@ class CreateActionTextTables < ActiveRecord::Migration[6.0]
def change
create_table :action_text_rich_texts do |t|
t.string :name, null: false
t.text :body, limit: 16777215
t.text :body, size: :long
t.references :record, null: false, polymorphic: true, index: false
t.timestamps

@ -2,11 +2,10 @@ class CreateActionTextTables < ActiveRecord::Migration[6.0]
def change
create_table :action_text_rich_texts do |t|
t.string :name, null: false
t.text :body, limit: 16777215
t.text :body, size: :long
t.references :record, null: false, polymorphic: true, index: false
t.datetime :created_at, null: false
t.datetime :updated_at, null: false
t.timestamps
t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true
end

@ -14,7 +14,7 @@
create_table "action_text_rich_texts", force: :cascade do |t|
t.string "name", null: false
t.text "body", limit: 16777215
t.text "body"
t.string "record_type", null: false
t.integer "record_id", null: false
t.datetime "created_at", precision: 6, null: false

@ -1,3 +1,7 @@
* MySQL: Support `:size` option to change text and blob size.
*Ryuta Kamizono*
* Make `t.timestamps` with precision by default.
*Ryuta Kamizono*

@ -29,7 +29,7 @@ class AbstractMysqlAdapter < AbstractAdapter
NATIVE_DATABASE_TYPES = {
primary_key: "bigint auto_increment PRIMARY KEY",
string: { name: "varchar", limit: 255 },
text: { name: "text", limit: 65535 },
text: { name: "text" },
integer: { name: "int", limit: 4 },
float: { name: "float", limit: 24 },
decimal: { name: "decimal" },
@ -37,7 +37,8 @@ class AbstractMysqlAdapter < AbstractAdapter
timestamp: { name: "timestamp" },
time: { name: "time" },
date: { name: "date" },
binary: { name: "blob", limit: 65535 },
binary: { name: "blob" },
blob: { name: "blob" },
boolean: { name: "tinyint", limit: 1 },
json: { name: "json" },
}

@ -56,6 +56,16 @@ def new_column_definition(name, type, **options) # :nodoc:
case type
when :virtual
type = options[:type]
when :text, :blob, :binary
case (size = options[:size])&.to_s
when "tiny", "medium", "long"
sql_type = @conn.native_database_types[type][:name]
type = "#{size}#{sql_type}"
else
raise ArgumentError, <<~MSG unless size.nil?
#{size.inspect} is invalid :size value. Only :tiny, :medium, and :long are allowed.
MSG
end
when :primary_key
type = :integer
options[:limit] ||= 8

@ -10,6 +10,10 @@ def prepare_column_options(column)
spec[:unsigned] = "true" if column.unsigned?
spec[:auto_increment] = "true" if column.auto_increment?
if /\A(?<size>tiny|medium|long)(?:text|blob)/ =~ column.sql_type
spec = { size: size.to_sym.inspect }.merge!(spec)
end
if @connection.supports_virtual_columns? && column.virtual?
spec[:as] = extract_expression_for_virtual_column(column)
spec[:stored] = "true" if /\b(?:STORED|PERSISTENT)\b/.match?(column.extra)
@ -37,13 +41,15 @@ def schema_type(column)
case column.sql_type
when /\Atimestamp\b/
:timestamp
when "tinyblob"
:blob
else
super
end
end
def schema_limit(column)
super unless /\A(?:tiny|medium|long)?(?:text|blob)/.match?(column.sql_type)
end
def schema_precision(column)
super unless /\A(?:date)?time(?:stamp)?\b/.match?(column.sql_type) && column.precision == 0
end

@ -626,6 +626,18 @@ def test_out_of_range_text_limit_should_raise
ensure
Person.connection.drop_table :test_text_limits, if_exists: true
end
def test_invalid_text_size_should_raise
e = assert_raise(ArgumentError) do
Person.connection.create_table :test_text_sizes, force: true do |t|
t.text :bigtext, size: 0xfffffffff
end
end
assert_match(/#{0xfffffffff} is invalid :size value\. Only :tiny, :medium, and :long are allowed\./, e.message)
ensure
Person.connection.drop_table :test_text_sizes, if_exists: true
end
end
if ActiveRecord::Base.connection.supports_advisory_locks?

@ -245,25 +245,31 @@ def test_schema_dump_expression_indices
if current_adapter?(:Mysql2Adapter)
def test_schema_dump_includes_length_for_mysql_binary_fields
output = standard_dump
output = dump_table_schema "binary_fields"
assert_match %r{t\.binary\s+"var_binary",\s+limit: 255$}, output
assert_match %r{t\.binary\s+"var_binary_large",\s+limit: 4095$}, output
end
def test_schema_dump_includes_length_for_mysql_blob_and_text_fields
output = standard_dump
assert_match %r{t\.blob\s+"tiny_blob",\s+limit: 255$}, output
output = dump_table_schema "binary_fields"
assert_match %r{t\.binary\s+"tiny_blob",\s+size: :tiny$}, output
assert_match %r{t\.binary\s+"normal_blob"$}, output
assert_match %r{t\.binary\s+"medium_blob",\s+limit: 16777215$}, output
assert_match %r{t\.binary\s+"long_blob",\s+limit: 4294967295$}, output
assert_match %r{t\.text\s+"tiny_text",\s+limit: 255$}, output
assert_match %r{t\.binary\s+"medium_blob",\s+size: :medium$}, output
assert_match %r{t\.binary\s+"long_blob",\s+size: :long$}, output
assert_match %r{t\.text\s+"tiny_text",\s+size: :tiny$}, output
assert_match %r{t\.text\s+"normal_text"$}, output
assert_match %r{t\.text\s+"medium_text",\s+limit: 16777215$}, output
assert_match %r{t\.text\s+"long_text",\s+limit: 4294967295$}, output
assert_match %r{t\.text\s+"medium_text",\s+size: :medium$}, output
assert_match %r{t\.text\s+"long_text",\s+size: :long$}, output
assert_match %r{t\.binary\s+"tiny_blob_2",\s+size: :tiny$}, output
assert_match %r{t\.binary\s+"medium_blob_2",\s+size: :medium$}, output
assert_match %r{t\.binary\s+"long_blob_2",\s+size: :long$}, output
assert_match %r{t\.text\s+"tiny_text_2",\s+size: :tiny$}, output
assert_match %r{t\.text\s+"medium_text_2",\s+size: :medium$}, output
assert_match %r{t\.text\s+"long_text_2",\s+size: :long$}, output
end
def test_schema_does_not_include_limit_for_emulated_mysql_boolean_fields
output = standard_dump
output = dump_table_schema "booleans"
assert_no_match %r{t\.boolean\s+"has_fun",.+limit: 1}, output
end

@ -27,6 +27,7 @@
create_table :binary_fields, force: true do |t|
t.binary :var_binary, limit: 255
t.binary :var_binary_large, limit: 4095
t.tinyblob :tiny_blob
t.blob :normal_blob
t.mediumblob :medium_blob
@ -36,6 +37,13 @@
t.mediumtext :medium_text
t.longtext :long_text
t.binary :tiny_blob_2, size: :tiny
t.binary :medium_blob_2, size: :medium
t.binary :long_blob_2, size: :long
t.text :tiny_text_2, size: :tiny
t.text :medium_text_2, size: :medium
t.text :long_text_2, size: :long
t.index :var_binary
end