Correctly dump native timestamp types for MySQL

The native timestamp type in MySQL is different from datetime type.
Internal representation of the timestamp type is UNIX time, This means
that timestamp columns are affected by time zone.

```
> SET time_zone = '+00:00';
Query OK, 0 rows affected (0.00 sec)

> INSERT INTO time_with_zone(ts,dt) VALUES (NOW(),NOW());
Query OK, 1 row affected (0.02 sec)

> SELECT * FROM time_with_zone;
+---------------------+---------------------+
| ts                  | dt                  |
+---------------------+---------------------+
| 2016-02-07 22:11:44 | 2016-02-07 22:11:44 |
+---------------------+---------------------+
1 row in set (0.00 sec)

> SET time_zone = '-08:00';
Query OK, 0 rows affected (0.00 sec)

> SELECT * FROM time_with_zone;
+---------------------+---------------------+
| ts                  | dt                  |
+---------------------+---------------------+
| 2016-02-07 14:11:44 | 2016-02-07 22:11:44 |
+---------------------+---------------------+
1 row in set (0.00 sec)
```
This commit is contained in:
Ryuta Kamizono 2015-10-11 21:45:55 +09:00
parent 45468b87f1
commit 505526335b
10 changed files with 80 additions and 5 deletions

View File

@ -1,3 +1,36 @@
* Correctly dump native timestamp types for MySQL.
The native timestamp type in MySQL is different from datetime type.
Internal representation of the timestamp type is UNIX time, This means
that timestamp columns are affected by time zone.
> SET time_zone = '+00:00';
Query OK, 0 rows affected (0.00 sec)
> INSERT INTO time_with_zone(ts,dt) VALUES (NOW(),NOW());
Query OK, 1 row affected (0.02 sec)
> SELECT * FROM time_with_zone;
+---------------------+---------------------+
| ts | dt |
+---------------------+---------------------+
| 2016-02-07 22:11:44 | 2016-02-07 22:11:44 |
+---------------------+---------------------+
1 row in set (0.00 sec)
> SET time_zone = '-08:00';
Query OK, 0 rows affected (0.00 sec)
> SELECT * FROM time_with_zone;
+---------------------+---------------------+
| ts | dt |
+---------------------+---------------------+
| 2016-02-07 14:11:44 | 2016-02-07 22:11:44 |
+---------------------+---------------------+
1 row in set (0.00 sec)
*Ryuta Kamizono*
* All integer-like PKs are autoincrement unless they have an explicit default.
*Matthew Draper*

View File

@ -1071,7 +1071,7 @@ module ActiveRecord
raise ArgumentError, "Error adding decimal column: precision cannot be empty if scale is specified"
end
elsif [:datetime, :time, :interval].include?(type) && precision ||= native[:precision]
elsif [:datetime, :timestamp, :time, :interval].include?(type) && precision ||= native[:precision]
if (0..6) === precision
column_type_sql << "(#{precision})"
else

View File

@ -46,6 +46,7 @@ module ActiveRecord
float: { name: "float" },
decimal: { name: "decimal" },
datetime: { name: "datetime" },
timestamp: { name: "timestamp" },
time: { name: "time" },
date: { name: "date" },
binary: { name: "blob", limit: 65535 },
@ -708,7 +709,7 @@ module ActiveRecord
end
def extract_precision(sql_type)
if /time/.match?(sql_type)
if /\A(?:date)?time(?:stamp)?\b/.match?(sql_type)
super || 0
else
super

View File

@ -25,6 +25,14 @@ module ActiveRecord
end
def add_column_options!(sql, options)
# By default, TIMESTAMP columns are NOT NULL, cannot contain NULL values,
# and assigning NULL assigns the current timestamp. To permit a TIMESTAMP
# column to contain NULL, explicitly declare it with the NULL attribute.
# See http://dev.mysql.com/doc/refman/5.7/en/timestamp-initialization.html
if /\Atimestamp\b/.match?(options[:column].sql_type) && !options[:primary_key]
sql << " NULL" unless options[:null] == false || options_include_default?(options)
end
if charset = options[:charset]
sql << " CHARACTER SET #{charset}"
end

View File

@ -75,6 +75,11 @@ module ActiveRecord
super
end
private
def aliased_types(name, fallback)
fallback
end
end
class Table < ActiveRecord::ConnectionAdapters::Table

View File

@ -30,7 +30,10 @@ module ActiveRecord
end
def schema_type(column)
if column.sql_type == "tinyblob"
case column.sql_type
when /\Atimestamp\b/
:timestamp
when "tinyblob"
:blob
else
super
@ -38,7 +41,7 @@ module ActiveRecord
end
def schema_precision(column)
super unless /time/.match?(column.sql_type) && column.precision == 0
super unless /\A(?:date)?time(?:stamp)?\b/.match?(column.sql_type) && column.precision == 0
end
def schema_collation(column)

View File

@ -100,11 +100,21 @@ if current_adapter?(:Mysql2Adapter)
include SchemaDumpingHelper
if ActiveRecord::Base.connection.version >= "5.6.0"
test "schema dump includes default expression" do
test "schema dump datetime includes default expression" do
output = dump_table_schema("datetime_defaults")
assert_match %r/t\.datetime\s+"modified_datetime",\s+default: -> { "CURRENT_TIMESTAMP" }/, output
end
end
test "schema dump timestamp includes default expression" do
output = dump_table_schema("timestamp_defaults")
assert_match %r/t\.timestamp\s+"modified_timestamp",\s+default: -> { "CURRENT_TIMESTAMP" }/, output
end
test "schema dump timestamp without default expression" do
output = dump_table_schema("timestamp_defaults")
assert_match %r/t\.timestamp\s+"nullable_timestamp"$/, output
end
end
class DefaultsTestWithoutTransactionalFixtures < ActiveRecord::TestCase

View File

@ -269,6 +269,8 @@ module ActiveRecord
if current_adapter?(:PostgreSQLAdapter)
assert_equal "timestamp without time zone", klass.columns_hash["foo"].sql_type
elsif current_adapter?(:Mysql2Adapter)
assert_equal "timestamp", klass.columns_hash["foo"].sql_type
else
assert_equal klass.connection.type_to_sql("datetime"), klass.columns_hash["foo"].sql_type
end

View File

@ -291,6 +291,14 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase
schema = dump_table_schema "barcodes"
assert_match %r{create_table "barcodes", primary_key: "code", id: :string, limit: 42}, schema
end
if current_adapter?(:Mysql2Adapter) && subsecond_precision_supported?
test "schema typed primary key column" do
@connection.create_table(:scheduled_logs, id: :timestamp, precision: 6, force: true)
schema = dump_table_schema("scheduled_logs")
assert_match %r/create_table "scheduled_logs", id: :timestamp, precision: 6/, schema
end
end
end
class CompositePrimaryKeyTest < ActiveRecord::TestCase

View File

@ -6,6 +6,11 @@ ActiveRecord::Schema.define do
end
end
create_table :timestamp_defaults, force: true do |t|
t.timestamp :nullable_timestamp
t.timestamp :modified_timestamp, default: -> { "CURRENT_TIMESTAMP" }
end
create_table :binary_fields, force: true do |t|
t.binary :var_binary, limit: 255
t.binary :var_binary_large, limit: 4095