Fix type casting column default in change_column

Since #31230, `change_column` is executed as a bulk statement.

That caused incorrect type casting column default by looking up the
before changed type, not the after changed type.

In a bulk statement, we can't use `change_column_default_for_alter` if
the statement changes the column type.

This fixes the type casting to use the constructed target sql_type.

Fixes #34938.
This commit is contained in:
Ryuta Kamizono 2019-01-20 10:56:17 +09:00
parent dd8d37881c
commit 4a0e3809be
4 changed files with 56 additions and 28 deletions

@ -17,6 +17,42 @@ def visit_ValidateConstraint(name)
"VALIDATE CONSTRAINT #{quote_column_name(name)}"
end
def visit_ChangeColumnDefinition(o)
column = o.column
column.sql_type = type_to_sql(column.type, column.options)
quoted_column_name = quote_column_name(o.name)
change_column_sql = +"ALTER COLUMN #{quoted_column_name} TYPE #{column.sql_type}"
options = column_options(column)
if options[:collation]
change_column_sql << " COLLATE \"#{options[:collation]}\""
end
if options[:using]
change_column_sql << " USING #{options[:using]}"
elsif options[:cast_as]
cast_as_type = type_to_sql(options[:cast_as], options)
change_column_sql << " USING CAST(#{quoted_column_name} AS #{cast_as_type})"
end
if options.key?(:default)
if options[:default].nil?
change_column_sql << ", ALTER COLUMN #{quoted_column_name} DROP DEFAULT"
else
quoted_default = quote_default_expression(options[:default], column)
change_column_sql << ", ALTER COLUMN #{quoted_column_name} SET DEFAULT #{quoted_default}"
end
end
if options.key?(:null)
change_column_sql << ", ALTER COLUMN #{quoted_column_name} #{options[:null] ? 'DROP' : 'SET'} NOT NULL"
end
change_column_sql
end
def add_column_options!(sql, options)
if options[:collation]
sql << " COLLATE \"#{options[:collation]}\""

@ -683,38 +683,20 @@ def extract_foreign_key_action(specifier)
end
end
def change_column_sql(table_name, column_name, type, options = {})
quoted_column_name = quote_column_name(column_name)
sql_type = type_to_sql(type, options)
sql = +"ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}"
if options[:collation]
sql << " COLLATE \"#{options[:collation]}\""
end
if options[:using]
sql << " USING #{options[:using]}"
elsif options[:cast_as]
cast_as_type = type_to_sql(options[:cast_as], options)
sql << " USING CAST(#{quoted_column_name} AS #{cast_as_type})"
end
sql
end
def add_column_for_alter(table_name, column_name, type, options = {})
return super unless options.key?(:comment)
[super, Proc.new { change_column_comment(table_name, column_name, options[:comment]) }]
end
def change_column_for_alter(table_name, column_name, type, options = {})
sqls = [change_column_sql(table_name, column_name, type, options)]
sqls << change_column_default_for_alter(table_name, column_name, options[:default]) if options.key?(:default)
sqls << change_column_null_for_alter(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
td = create_table_definition(table_name)
cd = td.new_column_definition(column_name, type, options)
sqls = [schema_creation.accept(ChangeColumnDefinition.new(cd, column_name))]
sqls << Proc.new { change_column_comment(table_name, column_name, options[:comment]) } if options.key?(:comment)
sqls
end
# Changes the default value of a table column.
def change_column_default_for_alter(table_name, column_name, default_or_changes) # :nodoc:
def change_column_default_for_alter(table_name, column_name, default_or_changes)
column = column_for(table_name, column_name)
return unless column
@ -729,8 +711,8 @@ def change_column_default_for_alter(table_name, column_name, default_or_changes)
end
end
def change_column_null_for_alter(table_name, column_name, null, default = nil) #:nodoc:
"ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL"
def change_column_null_for_alter(table_name, column_name, null, default = nil)
"ALTER COLUMN #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL"
end
def add_timestamps_for_alter(table_name, options = {})

@ -36,9 +36,7 @@ class << recorder
class V5_1 < V5_2
def change_column(table_name, column_name, type, options = {})
if adapter_name == "PostgreSQL"
clear_cache!
sql = connection.send(:change_column_sql, table_name, column_name, type, options)
execute "ALTER TABLE #{quote_table_name(table_name)} #{sql}"
super(table_name, column_name, type, options.except(:default, :null, :comment))
change_column_default(table_name, column_name, options[:default]) if options.key?(:default)
change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment)

@ -17,7 +17,7 @@ def setup
enable_extension!("hstore", @connection)
@connection.transaction do
@connection.create_table("pg_arrays") do |t|
@connection.create_table "pg_arrays", force: true do |t|
t.string "tags", array: true, limit: 255
t.integer "ratings", array: true
t.datetime :datetimes, array: true
@ -112,6 +112,18 @@ def test_change_column_with_array
assert_predicate column, :array?
end
def test_change_column_from_non_array_to_array
@connection.add_column :pg_arrays, :snippets, :string
@connection.change_column :pg_arrays, :snippets, :text, array: true, default: [], using: "string_to_array(\"snippets\", ',')"
PgArray.reset_column_information
column = PgArray.columns_hash["snippets"]
assert_equal :text, column.type
assert_equal [], PgArray.column_defaults["snippets"]
assert_predicate column, :array?
end
def test_change_column_cant_make_non_array_column_to_array
@connection.add_column :pg_arrays, :a_string, :string
assert_raises ActiveRecord::StatementInvalid do