From 4a0e3809be480cd3e0ca503d451b966259b232e9 Mon Sep 17 00:00:00 2001
From: Ryuta Kamizono <kamipo@gmail.com>
Date: Sun, 20 Jan 2019 10:56:17 +0900
Subject: 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.
---
 .../postgresql/schema_creation.rb                  | 36 ++++++++++++++++++++++
 .../postgresql/schema_statements.rb                | 30 ++++--------------
 .../lib/active_record/migration/compatibility.rb   |  4 +--
 .../test/cases/adapters/postgresql/array_test.rb   | 14 ++++++++-
 4 files changed, 56 insertions(+), 28 deletions(-)

diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb
index ceb8b40bd9..84dd28907b 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb
@@ -17,6 +17,42 @@ module ActiveRecord
             "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]}\""
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
index 3516bef75a..7cf371be68 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -683,38 +683,20 @@ module ActiveRecord
             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 @@ module ActiveRecord
             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 = {})
diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb
index 8f6fcfcaea..608182e363 100644
--- a/activerecord/lib/active_record/migration/compatibility.rb
+++ b/activerecord/lib/active_record/migration/compatibility.rb
@@ -36,9 +36,7 @@ module ActiveRecord
       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)
diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb
index b12055a40a..2e7a4b498f 100644
--- a/activerecord/test/cases/adapters/postgresql/array_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/array_test.rb
@@ -17,7 +17,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
     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 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
     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
-- 
cgit v1.2.3