Active Record + PostgreSQL: native support for timestamp with time zone
In https://github.com/rails/rails/issues/21126 it was suggested to make "timestamp with time zone" the default type for datetime columns in PostgreSQL. This is in line with PostgreSQL [best practices](https://wiki.postgresql.org/wiki/Don't_Do_This#Don.27t_use_timestamp_.28without_time_zone.29). This PR lays some groundwork for that. This PR adds a configuration option, `ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_type`. The default is `:timestamp` which preserves current Rails behavior of using "timestamp without time zone" when you do `t.datetime` in a migration. If you change it to `:timestamptz`, you'll get "timestamp with time zone" columns instead. If you change this setting in an existing app, you should immediately call `bin/rails db:migrate` to ensure your `schema.rb` file remains correct. If you do so, then existing columns will not be impacted, so for example if you have an app with a mixture of both types of columns, and you change the config, schema dumps will continue to output the correct types. This PR also adds two new types that can be used in migrations: `t.timestamp` and `t.timestamptz`. ```ruby ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_type = :timestamp # default value is :timestamp create_table("foo1") do |t| t.datetime :default_format # "timestamp without time zone" t.timestamp :without_time_zone # "timestamp without time zone" t.timestamptz :with_time_zone # "timestamp with time zone" end ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_type = :timestamptz create_table("foo2") do |t| t.datetime :default_format # "timestamp with time zone" <-- note how this has changed! t.timestamp :without_time_zone # "timestamp without time zone" t.timestamptz :with_time_zone # "timestamp with time zone" end ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::NATIVE_DATABASE_TYPES[:my_custom_type] = { name: "custom_datetime_format_i_invented" } ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_type = :my_custom_type create_table("foo3") do |t| t.datetime :default_format # "custom_datetime_format_i_invented" t.timestamp :without_time_zone # "timestamp without time zone" t.timestamptz :with_time_zone # "timestamp with time zone" end ``` **Notes** - This PR doesn't change the default `datetime` format. The default is still "timestamp without time zone". A future PR could do that, but there was enough code here just getting the config option right. - See also https://github.com/rails/rails/pull/41395 which set some groundwork (and added some tests) for this. - This reverts some of https://github.com/rails/rails/pull/15184. https://github.com/rails/rails/pull/15184 alluded to issues in XML serialization, but I couldn't find any related tests that this broke.
This commit is contained in:
parent
d89d14d241
commit
867d27dd64
@ -404,6 +404,18 @@
|
||||
|
||||
*Josua Schmid*
|
||||
|
||||
* PostgreSQL: introduce `ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_type`
|
||||
|
||||
This setting controls what native type Active Record should use when you call `datetime` in
|
||||
a migration or schema. It takes a symbol which must correspond to one of the configured
|
||||
`NATIVE_DATABASE_TYPES`. The default is `:timestamp`, meaning `t.datetime` in a migration
|
||||
will create a "timestamp without time zone" column. To use "timestamp with time zone",
|
||||
change this to `:timestamptz` in an initializer.
|
||||
|
||||
You should run `bin/rails db:migrate` to rebuild your schema.rb if you change this.
|
||||
|
||||
*Alex Ghiculescu*
|
||||
|
||||
* PostgreSQL: handle `timestamp with time zone` columns correctly in `schema.rb`.
|
||||
|
||||
Previously they dumped as `t.datetime :column_name`, now they dump as `t.timestamptz :column_name`,
|
||||
|
@ -24,6 +24,11 @@ def type_cast_for_schema(value)
|
||||
else super
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
def real_type_unless_aliased(real_type)
|
||||
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_type == real_type ? :datetime : real_type
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -5,6 +5,9 @@ module ConnectionAdapters
|
||||
module PostgreSQL
|
||||
module OID # :nodoc:
|
||||
class Timestamp < DateTime # :nodoc:
|
||||
def type
|
||||
real_type_unless_aliased(:timestamp)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -4,9 +4,22 @@ module ActiveRecord
|
||||
module ConnectionAdapters
|
||||
module PostgreSQL
|
||||
module OID # :nodoc:
|
||||
class TimestampWithTimeZone < Timestamp # :nodoc:
|
||||
class TimestampWithTimeZone < DateTime # :nodoc:
|
||||
def type
|
||||
:timestamptz
|
||||
real_type_unless_aliased(:timestamptz)
|
||||
end
|
||||
|
||||
def cast_value(value)
|
||||
time = super
|
||||
return time if time.is_a?(ActiveSupport::TimeWithZone)
|
||||
|
||||
# While in UTC mode, the PG gem may not return times back in "UTC" even if they were provided to Postgres in UTC.
|
||||
# We prefer times always in UTC, so here we convert back.
|
||||
if is_utc?
|
||||
time.getutc
|
||||
else
|
||||
time.getlocal
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -192,6 +192,10 @@ def initialize(*, **)
|
||||
end
|
||||
|
||||
private
|
||||
def aliased_types(name, fallback)
|
||||
fallback
|
||||
end
|
||||
|
||||
def integer_like_primary_key_type(type, options)
|
||||
if type == :bigint || options[:limit] == 8
|
||||
:bigserial
|
||||
|
@ -98,6 +98,24 @@ def new_client(conn_params)
|
||||
# ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = true
|
||||
class_attribute :create_unlogged_tables, default: false
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# PostgreSQL supports multiple types for DateTimes. By default if you use `datetime`
|
||||
# in migrations, Rails will translate this to a PostgreSQL "timestamp without time zone".
|
||||
# Change this in an initializer to use another NATIVE_DATABASE_TYPES. For example, to
|
||||
# store DateTimes as "timestamp with time zone":
|
||||
#
|
||||
# ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_type = :timestamptz
|
||||
#
|
||||
# Or if you are adding a custom type:
|
||||
#
|
||||
# ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::NATIVE_DATABASE_TYPES[:my_custom_type] = { name: "my_custom_type_name" }
|
||||
# ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_type = :my_custom_type
|
||||
#
|
||||
# If you're using :ruby as your config.active_record.schema_format and you change this
|
||||
# setting, you should immediately run bin/rails db:migrate to update the types in your schema.rb.
|
||||
class_attribute :datetime_type, default: :timestamp
|
||||
|
||||
NATIVE_DATABASE_TYPES = {
|
||||
primary_key: "bigserial primary key",
|
||||
string: { name: "character varying" },
|
||||
@ -105,7 +123,8 @@ def new_client(conn_params)
|
||||
integer: { name: "integer", limit: 4 },
|
||||
float: { name: "float" },
|
||||
decimal: { name: "decimal" },
|
||||
datetime: { name: "timestamp" },
|
||||
datetime: {}, # set dynamically based on datetime_type
|
||||
timestamp: { name: "timestamp" },
|
||||
timestamptz: { name: "timestamptz" },
|
||||
time: { name: "time" },
|
||||
date: { name: "date" },
|
||||
@ -319,7 +338,15 @@ def discard! # :nodoc:
|
||||
end
|
||||
|
||||
def native_database_types #:nodoc:
|
||||
NATIVE_DATABASE_TYPES
|
||||
self.class.native_database_types
|
||||
end
|
||||
|
||||
def self.native_database_types #:nodoc:
|
||||
@native_database_types ||= begin
|
||||
types = NATIVE_DATABASE_TYPES.dup
|
||||
types[:datetime] = types[datetime_type]
|
||||
types
|
||||
end
|
||||
end
|
||||
|
||||
def set_standard_conforming_strings
|
||||
@ -889,7 +916,12 @@ def update_typemap_for_default_timezone
|
||||
|
||||
@timestamp_decoder = decoder_class.new(@timestamp_decoder.to_h)
|
||||
@connection.type_map_for_results.add_coder(@timestamp_decoder)
|
||||
|
||||
@default_timezone = ActiveRecord::Base.default_timezone
|
||||
|
||||
# if default timezone has changed, we need to reconfigure the connection
|
||||
# (specifically, the session time zone)
|
||||
configure_connection
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -101,7 +101,7 @@ def self.configurations
|
||||
##
|
||||
# :singleton-method:
|
||||
# Specify whether schema dump should happen at the end of the
|
||||
# db:migrate rails command. This is true by default, which is useful for the
|
||||
# bin/rails db:migrate command. This is true by default, which is useful for the
|
||||
# development environment. This should ideally be false in the production
|
||||
# environment where dumping schema is rarely needed.
|
||||
mattr_accessor :dump_schema_after_migration, instance_writer: false, default: true
|
||||
|
@ -16,6 +16,47 @@ def self.find(version)
|
||||
V7_0 = Current
|
||||
|
||||
class V6_1 < V7_0
|
||||
class PostgreSQLCompat
|
||||
def self.compatible_timestamp_type(type, connection)
|
||||
if connection.adapter_name == "PostgreSQL"
|
||||
# For Rails <= 6.1, :datetime was aliased to :timestamp
|
||||
# See: https://github.com/rails/rails/blob/v6.1.3.2/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L108
|
||||
# From Rails 7 onwards, you can define what :datetime resolves to (the default is still :timestamp)
|
||||
# See `ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_type`
|
||||
type.to_sym == :datetime ? :timestamp : type
|
||||
else
|
||||
type
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def add_column(table_name, column_name, type, **options)
|
||||
type = PostgreSQLCompat.compatible_timestamp_type(type, connection)
|
||||
super
|
||||
end
|
||||
|
||||
def create_table(table_name, **options)
|
||||
if block_given?
|
||||
super { |t| yield compatible_table_definition(t) }
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
module TableDefinition
|
||||
def new_column_definition(name, type, **options)
|
||||
type = PostgreSQLCompat.compatible_timestamp_type(type, @conn)
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def compatible_table_definition(t)
|
||||
class << t
|
||||
prepend TableDefinition
|
||||
end
|
||||
t
|
||||
end
|
||||
end
|
||||
|
||||
class V6_0 < V6_1
|
||||
|
@ -29,6 +29,37 @@ def test_change_type_with_symbol
|
||||
assert_equal :datetime, connection.columns(:strings).find { |c| c.name == "somedate" }.type
|
||||
end
|
||||
|
||||
def test_change_type_with_symbol_with_timestamptz
|
||||
connection.change_column :strings, :somedate, :timestamptz, cast_as: :timestamptz
|
||||
assert_equal :timestamptz, connection.columns(:strings).find { |c| c.name == "somedate" }.type
|
||||
end
|
||||
|
||||
def test_change_type_with_symbol_using_datetime
|
||||
connection.change_column :strings, :somedate, :datetime, cast_as: :datetime
|
||||
assert_equal :datetime, connection.columns(:strings).find { |c| c.name == "somedate" }.type
|
||||
end
|
||||
|
||||
def test_change_type_with_symbol_using_timestamp_with_timestamptz_as_default
|
||||
with_postgresql_datetime_type(:timestamptz) do
|
||||
connection.change_column :strings, :somedate, :timestamp, cast_as: :timestamp
|
||||
assert_equal :timestamp, connection.columns(:strings).find { |c| c.name == "somedate" }.type
|
||||
end
|
||||
end
|
||||
|
||||
def test_change_type_with_symbol_with_timestamptz_as_default
|
||||
with_postgresql_datetime_type(:timestamptz) do
|
||||
connection.change_column :strings, :somedate, :timestamptz, cast_as: :timestamptz
|
||||
assert_equal :datetime, connection.columns(:strings).find { |c| c.name == "somedate" }.type
|
||||
end
|
||||
end
|
||||
|
||||
def test_change_type_with_symbol_using_datetime_with_timestamptz_as_default
|
||||
with_postgresql_datetime_type(:timestamptz) do
|
||||
connection.change_column :strings, :somedate, :datetime, cast_as: :datetime
|
||||
assert_equal :datetime, connection.columns(:strings).find { |c| c.name == "somedate" }.type
|
||||
end
|
||||
end
|
||||
|
||||
def test_change_type_with_array
|
||||
connection.change_column :strings, :somedate, :timestamp, array: true, cast_as: :timestamp
|
||||
column = connection.columns(:strings).find { |c| c.name == "somedate" }
|
||||
|
@ -90,3 +90,48 @@ def test_bc_timestamp_year_zero
|
||||
assert_equal date, Developer.find_by_name("yahagi").updated_at
|
||||
end
|
||||
end
|
||||
|
||||
class PostgresqlTimestampMigrationTest < ActiveRecord::PostgreSQLTestCase
|
||||
class PostgresqlTimestampWithZone < ActiveRecord::Base; end
|
||||
|
||||
def test_adds_column_as_timestamp
|
||||
original, $stdout = $stdout, StringIO.new
|
||||
|
||||
ActiveRecord::Migration.new.add_column :postgresql_timestamp_with_zones, :times, :datetime
|
||||
|
||||
assert_equal({ "data_type" => "timestamp without time zone" },
|
||||
PostgresqlTimestampWithZone.connection.execute("select data_type from information_schema.columns where column_name = 'times'").to_a.first)
|
||||
ensure
|
||||
$stdout = original
|
||||
end
|
||||
|
||||
def test_adds_column_as_timestamptz_if_datetime_type_changed
|
||||
original, $stdout = $stdout, StringIO.new
|
||||
|
||||
with_postgresql_datetime_type(:timestamptz) do
|
||||
ActiveRecord::Migration.new.add_column :postgresql_timestamp_with_zones, :times, :datetime
|
||||
|
||||
assert_equal({ "data_type" => "timestamp with time zone" },
|
||||
PostgresqlTimestampWithZone.connection.execute("select data_type from information_schema.columns where column_name = 'times'").to_a.first)
|
||||
end
|
||||
ensure
|
||||
$stdout = original
|
||||
end
|
||||
|
||||
def test_adds_column_as_custom_type
|
||||
original, $stdout = $stdout, StringIO.new
|
||||
|
||||
PostgresqlTimestampWithZone.connection.execute("CREATE TYPE custom_time_format AS ENUM ('past', 'present', 'future');")
|
||||
|
||||
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::NATIVE_DATABASE_TYPES[:datetimes_as_enum] = { name: "custom_time_format" }
|
||||
with_postgresql_datetime_type(:datetimes_as_enum) do
|
||||
ActiveRecord::Migration.new.add_column :postgresql_timestamp_with_zones, :times, :datetime
|
||||
|
||||
assert_equal({ "data_type" => "USER-DEFINED", "udt_name" => "custom_time_format" },
|
||||
PostgresqlTimestampWithZone.connection.execute("select data_type, udt_name from information_schema.columns where column_name = 'times'").to_a.first)
|
||||
end
|
||||
ensure
|
||||
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::NATIVE_DATABASE_TYPES.delete(:datetimes_as_enum)
|
||||
$stdout = original
|
||||
end
|
||||
end
|
||||
|
@ -117,12 +117,14 @@ def test_timestamps_with_and_without_zones
|
||||
create_table :has_timestamps do |t|
|
||||
t.datetime "default_format"
|
||||
t.datetime "without_time_zone"
|
||||
t.timestamp "also_without_time_zone"
|
||||
t.timestamptz "with_time_zone"
|
||||
end
|
||||
end
|
||||
|
||||
assert @connection.column_exists?(:has_timestamps, :default_format, :datetime)
|
||||
assert @connection.column_exists?(:has_timestamps, :without_time_zone, :datetime)
|
||||
assert @connection.column_exists?(:has_timestamps, :also_without_time_zone, :datetime)
|
||||
assert @connection.column_exists?(:has_timestamps, :with_time_zone, :timestamptz)
|
||||
end
|
||||
end
|
||||
|
@ -100,12 +100,77 @@ def test_formatting_datetime_according_to_precision
|
||||
|
||||
assert foo = Foo.find_by(created_at: date)
|
||||
assert_equal 1, Foo.where(updated_at: date).count
|
||||
assert_equal date.to_i, foo.created_at.to_i
|
||||
assert_equal date.to_s, foo.created_at.to_s
|
||||
assert_equal date.to_s, foo.updated_at.to_s
|
||||
assert_equal 000000, foo.created_at.usec
|
||||
assert_equal 999900, foo.updated_at.usec
|
||||
end
|
||||
|
||||
def test_formatting_datetime_according_to_precision_when_time_zone_aware
|
||||
with_timezone_config aware_attributes: true, zone: "Pacific Time (US & Canada)" do
|
||||
@connection.create_table(:foos, force: true) do |t|
|
||||
t.datetime :created_at, precision: 0
|
||||
t.datetime :updated_at, precision: 4
|
||||
end
|
||||
|
||||
date = ::Time.utc(2014, 8, 17, 12, 30, 0, 999999)
|
||||
Foo.create!(created_at: date, updated_at: date)
|
||||
|
||||
assert foo = Foo.find_by(created_at: date)
|
||||
assert_equal 1, Foo.where(updated_at: date).count
|
||||
assert_equal date.to_i, foo.created_at.to_i
|
||||
assert_equal date.in_time_zone.to_s, foo.created_at.to_s
|
||||
assert_equal date.in_time_zone.to_s, foo.updated_at.to_s
|
||||
assert_equal 000000, foo.created_at.usec
|
||||
assert_equal 999900, foo.updated_at.usec
|
||||
end
|
||||
end
|
||||
|
||||
if current_adapter?(:PostgreSQLAdapter)
|
||||
def test_formatting_datetime_according_to_precision_using_timestamptz
|
||||
with_postgresql_datetime_type(:timestamptz) do
|
||||
@connection.create_table(:foos, force: true) do |t|
|
||||
t.datetime :created_at, precision: 0
|
||||
t.datetime :updated_at, precision: 4
|
||||
end
|
||||
|
||||
date = ::Time.utc(2014, 8, 17, 12, 30, 0, 999999)
|
||||
Foo.create!(created_at: date, updated_at: date)
|
||||
|
||||
assert foo = Foo.find_by(created_at: date)
|
||||
assert_equal 1, Foo.where(updated_at: date).count
|
||||
assert_equal date.to_i, foo.created_at.to_i
|
||||
assert_equal date.to_s, foo.created_at.to_s
|
||||
assert_equal date.to_s, foo.updated_at.to_s
|
||||
assert_equal 000000, foo.created_at.usec
|
||||
assert_equal 999900, foo.updated_at.usec
|
||||
end
|
||||
end
|
||||
|
||||
def test_formatting_datetime_according_to_precision_when_time_zone_aware_using_timestamptz
|
||||
with_postgresql_datetime_type(:timestamptz) do
|
||||
with_timezone_config aware_attributes: true, zone: "Pacific Time (US & Canada)" do
|
||||
@connection.create_table(:foos, force: true) do |t|
|
||||
t.datetime :created_at, precision: 0
|
||||
t.datetime :updated_at, precision: 4
|
||||
end
|
||||
|
||||
date = ::Time.utc(2014, 8, 17, 12, 30, 0, 999999)
|
||||
Foo.create!(created_at: date, updated_at: date)
|
||||
|
||||
assert foo = Foo.find_by(created_at: date)
|
||||
assert_equal 1, Foo.where(updated_at: date).count
|
||||
assert_equal date.to_i, foo.created_at.to_i
|
||||
assert_equal date.in_time_zone.to_s, foo.created_at.to_s
|
||||
assert_equal date.in_time_zone.to_s, foo.updated_at.to_s
|
||||
assert_equal 000000, foo.created_at.usec
|
||||
assert_equal 999900, foo.updated_at.usec
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_schema_dump_includes_datetime_precision
|
||||
@connection.create_table(:foos, force: true) do |t|
|
||||
t.timestamps precision: 6
|
||||
|
@ -290,6 +290,39 @@ def test_add_column_with_timestamp_type
|
||||
end
|
||||
end
|
||||
|
||||
def test_add_column_with_postgresql_datetime_type
|
||||
connection.create_table :testings do |t|
|
||||
t.column :foo, :datetime
|
||||
end
|
||||
|
||||
column = connection.columns(:testings).find { |c| c.name == "foo" }
|
||||
|
||||
assert_equal :datetime, column.type
|
||||
|
||||
if current_adapter?(:PostgreSQLAdapter)
|
||||
assert_equal "timestamp without time zone", column.sql_type
|
||||
elsif current_adapter?(:Mysql2Adapter)
|
||||
assert_equal "datetime", column.sql_type
|
||||
else
|
||||
assert_equal connection.type_to_sql("datetime"), column.sql_type
|
||||
end
|
||||
end
|
||||
|
||||
if current_adapter?(:PostgreSQLAdapter)
|
||||
def test_add_column_with_datetime_in_timestamptz_mode
|
||||
with_postgresql_datetime_type(:timestamptz) do
|
||||
connection.create_table :testings do |t|
|
||||
t.column :foo, :datetime
|
||||
end
|
||||
|
||||
column = connection.columns(:testings).find { |c| c.name == "foo" }
|
||||
|
||||
assert_equal :datetime, column.type
|
||||
assert_equal "timestamp with time zone", column.sql_type
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_change_column_with_timestamp_type
|
||||
connection.create_table :testings do |t|
|
||||
t.column :foo, :datetime, null: false
|
||||
|
@ -502,6 +502,125 @@ def change
|
||||
def test_schema_dump_with_correct_timestamp_types_via_create_table_and_t_column
|
||||
original, $stdout = $stdout, StringIO.new
|
||||
|
||||
migration = Class.new(ActiveRecord::Migration::Current) do
|
||||
def up
|
||||
create_table("timestamps") do |t|
|
||||
t.datetime :this_should_remain_datetime
|
||||
t.timestamp :this_is_an_alias_of_datetime
|
||||
t.column :without_time_zone, :timestamp
|
||||
t.column :with_time_zone, :timestamptz
|
||||
end
|
||||
end
|
||||
def down
|
||||
drop_table("timestamps")
|
||||
end
|
||||
end
|
||||
migration.migrate(:up)
|
||||
|
||||
output = perform_schema_dump
|
||||
assert output.include?('t.datetime "this_should_remain_datetime"')
|
||||
assert output.include?('t.datetime "this_is_an_alias_of_datetime"')
|
||||
assert output.include?('t.datetime "without_time_zone"')
|
||||
assert output.include?('t.timestamptz "with_time_zone"')
|
||||
ensure
|
||||
migration.migrate(:down)
|
||||
$stdout = original
|
||||
end
|
||||
|
||||
def test_schema_dump_with_timestamptz_datetime_format
|
||||
migration, original, $stdout = nil, $stdout, StringIO.new
|
||||
|
||||
with_postgresql_datetime_type(:timestamptz) do
|
||||
migration = Class.new(ActiveRecord::Migration::Current) do
|
||||
def up
|
||||
create_table("timestamps") do |t|
|
||||
t.datetime :this_should_remain_datetime
|
||||
t.timestamptz :this_is_an_alias_of_datetime
|
||||
t.column :without_time_zone, :timestamp
|
||||
t.column :with_time_zone, :timestamptz
|
||||
end
|
||||
end
|
||||
def down
|
||||
drop_table("timestamps")
|
||||
end
|
||||
end
|
||||
migration.migrate(:up)
|
||||
|
||||
output = perform_schema_dump
|
||||
assert output.include?('t.datetime "this_should_remain_datetime"')
|
||||
assert output.include?('t.datetime "this_is_an_alias_of_datetime"')
|
||||
assert output.include?('t.timestamp "without_time_zone"')
|
||||
assert output.include?('t.datetime "with_time_zone"')
|
||||
end
|
||||
ensure
|
||||
migration.migrate(:down)
|
||||
$stdout = original
|
||||
end
|
||||
|
||||
def test_timestamps_schema_dump_before_rails_7
|
||||
migration, original, $stdout = nil, $stdout, StringIO.new
|
||||
|
||||
migration = Class.new(ActiveRecord::Migration[6.1]) do
|
||||
def up
|
||||
create_table("timestamps") do |t|
|
||||
t.datetime :this_should_remain_datetime
|
||||
t.timestamp :this_is_an_alias_of_datetime
|
||||
t.column :this_is_also_an_alias_of_datetime, :timestamp
|
||||
end
|
||||
end
|
||||
def down
|
||||
drop_table("timestamps")
|
||||
end
|
||||
end
|
||||
migration.migrate(:up)
|
||||
|
||||
output = perform_schema_dump
|
||||
assert output.include?('t.datetime "this_should_remain_datetime"')
|
||||
assert output.include?('t.datetime "this_is_an_alias_of_datetime"')
|
||||
assert output.include?('t.datetime "this_is_also_an_alias_of_datetime"')
|
||||
ensure
|
||||
migration.migrate(:down)
|
||||
$stdout = original
|
||||
end
|
||||
|
||||
def test_timestamps_schema_dump_before_rails_7_with_timestamptz_setting
|
||||
migration, original, $stdout = nil, $stdout, StringIO.new
|
||||
|
||||
with_postgresql_datetime_type(:timestamptz) do
|
||||
migration = Class.new(ActiveRecord::Migration[6.1]) do
|
||||
def up
|
||||
create_table("timestamps") do |t|
|
||||
t.datetime :this_should_change_to_timestamp
|
||||
t.timestamp :this_should_stay_as_timestamp
|
||||
t.column :this_should_also_stay_as_timestamp, :timestamp
|
||||
end
|
||||
end
|
||||
def down
|
||||
drop_table("timestamps")
|
||||
end
|
||||
end
|
||||
migration.migrate(:up)
|
||||
|
||||
output = perform_schema_dump
|
||||
# Normally we'd write `t.datetime` here. But because you've changed the `datetime_type`
|
||||
# to something else, `t.datetime` now means `:timestamptz`. To ensure that old columns
|
||||
# are still created as a `:timestamp` we need to change what is written to the schema dump.
|
||||
#
|
||||
# Typically in Rails we handle this through Migration versioning (`ActiveRecord::Migration::Compatibility`)
|
||||
# but that doesn't work here because the schema dumper is not aware of which migration
|
||||
# a column was added in.
|
||||
assert output.include?('t.timestamp "this_should_change_to_timestamp"')
|
||||
assert output.include?('t.timestamp "this_should_stay_as_timestamp"')
|
||||
assert output.include?('t.timestamp "this_should_also_stay_as_timestamp"')
|
||||
end
|
||||
ensure
|
||||
migration.migrate(:down)
|
||||
$stdout = original
|
||||
end
|
||||
|
||||
def test_schema_dump_when_changing_datetime_type_for_an_existing_app
|
||||
original, $stdout = $stdout, StringIO.new
|
||||
|
||||
migration = Class.new(ActiveRecord::Migration::Current) do
|
||||
def up
|
||||
create_table("timestamps") do |t|
|
||||
@ -520,7 +639,16 @@ def down
|
||||
assert output.include?('t.datetime "default_format"')
|
||||
assert output.include?('t.datetime "without_time_zone"')
|
||||
assert output.include?('t.timestamptz "with_time_zone"')
|
||||
|
||||
datetime_type_was = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_type
|
||||
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_type = :timestamptz
|
||||
|
||||
output = perform_schema_dump
|
||||
assert output.include?('t.timestamp "default_format"')
|
||||
assert output.include?('t.timestamp "without_time_zone"')
|
||||
assert output.include?('t.datetime "with_time_zone"')
|
||||
ensure
|
||||
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_type = datetime_type_was
|
||||
migration.migrate(:down)
|
||||
$stdout = original
|
||||
end
|
||||
@ -533,6 +661,7 @@ def up
|
||||
create_table("timestamps") do |t|
|
||||
t.datetime :default_format
|
||||
t.datetime :without_time_zone
|
||||
t.timestamp :also_without_time_zone
|
||||
t.timestamptz :with_time_zone
|
||||
end
|
||||
end
|
||||
@ -545,6 +674,7 @@ def down
|
||||
output = perform_schema_dump
|
||||
assert output.include?('t.datetime "default_format"')
|
||||
assert output.include?('t.datetime "without_time_zone"')
|
||||
assert output.include?('t.datetime "also_without_time_zone"')
|
||||
assert output.include?('t.timestamptz "with_time_zone"')
|
||||
ensure
|
||||
migration.migrate(:down)
|
||||
@ -559,7 +689,8 @@ def up
|
||||
create_table("timestamps")
|
||||
|
||||
add_column :timestamps, :default_format, :datetime
|
||||
add_column :timestamps, :without_time_zone, :timestamp
|
||||
add_column :timestamps, :without_time_zone, :datetime
|
||||
add_column :timestamps, :also_without_time_zone, :timestamp
|
||||
add_column :timestamps, :with_time_zone, :timestamptz
|
||||
end
|
||||
def down
|
||||
@ -571,11 +702,104 @@ def down
|
||||
output = perform_schema_dump
|
||||
assert output.include?('t.datetime "default_format"')
|
||||
assert output.include?('t.datetime "without_time_zone"')
|
||||
assert output.include?('t.datetime "also_without_time_zone"')
|
||||
assert output.include?('t.timestamptz "with_time_zone"')
|
||||
ensure
|
||||
migration.migrate(:down)
|
||||
$stdout = original
|
||||
end
|
||||
|
||||
def test_schema_dump_with_correct_timestamp_types_via_add_column_before_rails_7
|
||||
original, $stdout = $stdout, StringIO.new
|
||||
|
||||
migration = Class.new(ActiveRecord::Migration[6.1]) do
|
||||
def up
|
||||
create_table("timestamps")
|
||||
|
||||
add_column :timestamps, :default_format, :datetime
|
||||
add_column :timestamps, :without_time_zone, :datetime
|
||||
add_column :timestamps, :also_without_time_zone, :timestamp
|
||||
end
|
||||
def down
|
||||
drop_table("timestamps")
|
||||
end
|
||||
end
|
||||
migration.migrate(:up)
|
||||
|
||||
output = perform_schema_dump
|
||||
assert output.include?('t.datetime "default_format"')
|
||||
assert output.include?('t.datetime "without_time_zone"')
|
||||
assert output.include?('t.datetime "also_without_time_zone"')
|
||||
ensure
|
||||
migration.migrate(:down)
|
||||
$stdout = original
|
||||
end
|
||||
|
||||
def test_schema_dump_with_correct_timestamp_types_via_add_column_before_rails_7_with_timestamptz_setting
|
||||
migration, original, $stdout = nil, $stdout, StringIO.new
|
||||
|
||||
with_postgresql_datetime_type(:timestamptz) do
|
||||
migration = Class.new(ActiveRecord::Migration[6.1]) do
|
||||
def up
|
||||
create_table("timestamps")
|
||||
|
||||
add_column :timestamps, :this_should_change_to_timestamp, :datetime
|
||||
add_column :timestamps, :this_should_stay_as_timestamp, :timestamp
|
||||
end
|
||||
def down
|
||||
drop_table("timestamps")
|
||||
end
|
||||
end
|
||||
migration.migrate(:up)
|
||||
|
||||
output = perform_schema_dump
|
||||
# Normally we'd write `t.datetime` here. But because you've changed the `datetime_type`
|
||||
# to something else, `t.datetime` now means `:timestamptz`. To ensure that old columns
|
||||
# are still created as a `:timestamp` we need to change what is written to the schema dump.
|
||||
#
|
||||
# Typically in Rails we handle this through Migration versioning (`ActiveRecord::Migration::Compatibility`)
|
||||
# but that doesn't work here because the schema dumper is not aware of which migration
|
||||
# a column was added in.
|
||||
assert output.include?('t.timestamp "this_should_change_to_timestamp"')
|
||||
assert output.include?('t.timestamp "this_should_stay_as_timestamp"')
|
||||
end
|
||||
ensure
|
||||
migration.migrate(:down)
|
||||
$stdout = original
|
||||
end
|
||||
|
||||
def test_schema_dump_with_correct_timestamp_types_via_add_column_with_type_as_string
|
||||
migration, original, $stdout = nil, $stdout, StringIO.new
|
||||
|
||||
with_postgresql_datetime_type(:timestamptz) do
|
||||
migration = Class.new(ActiveRecord::Migration[6.1]) do
|
||||
def up
|
||||
create_table("timestamps")
|
||||
|
||||
add_column :timestamps, :this_should_change_to_timestamp, "datetime"
|
||||
add_column :timestamps, :this_should_stay_as_timestamp, "timestamp"
|
||||
end
|
||||
def down
|
||||
drop_table("timestamps")
|
||||
end
|
||||
end
|
||||
migration.migrate(:up)
|
||||
|
||||
output = perform_schema_dump
|
||||
# Normally we'd write `t.datetime` here. But because you've changed the `datetime_type`
|
||||
# to something else, `t.datetime` now means `:timestamptz`. To ensure that old columns
|
||||
# are still created as a `:timestamp` we need to change what is written to the schema dump.
|
||||
#
|
||||
# Typically in Rails we handle this through Migration versioning (`ActiveRecord::Migration::Compatibility`)
|
||||
# but that doesn't work here because the schema dumper is not aware of which migration
|
||||
# a column was added in.
|
||||
assert output.include?('t.timestamp "this_should_change_to_timestamp"')
|
||||
assert output.include?('t.timestamp "this_should_stay_as_timestamp"')
|
||||
end
|
||||
ensure
|
||||
migration.migrate(:down)
|
||||
$stdout = original
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -101,6 +101,18 @@ def reset_callbacks(klass, kind)
|
||||
subclass.send("_#{kind}_callbacks=", old_callbacks[subclass])
|
||||
end
|
||||
end
|
||||
|
||||
def with_postgresql_datetime_type(type)
|
||||
adapter = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
|
||||
adapter.remove_instance_variable(:@native_database_types) if adapter.instance_variable_defined?(:@native_database_types)
|
||||
datetime_type_was = adapter.datetime_type
|
||||
adapter.datetime_type = type
|
||||
yield
|
||||
ensure
|
||||
adapter = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
|
||||
adapter.datetime_type = datetime_type_was
|
||||
adapter.remove_instance_variable(:@native_database_types) if adapter.instance_variable_defined?(:@native_database_types)
|
||||
end
|
||||
end
|
||||
|
||||
class PostgreSQLTestCase < TestCase
|
||||
|
@ -479,7 +479,7 @@ The MySQL adapter adds one additional configuration option:
|
||||
|
||||
* `ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans` controls whether Active Record will consider all `tinyint(1)` columns as booleans. Defaults to `true`.
|
||||
|
||||
The PostgreSQL adapter adds one additional configuration option:
|
||||
The PostgreSQL adapter adds two additional configuration options:
|
||||
|
||||
* `ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables`
|
||||
controls whether database tables created should be "unlogged," which can speed
|
||||
@ -487,6 +487,14 @@ The PostgreSQL adapter adds one additional configuration option:
|
||||
highly recommended that you do not enable this in a production environment.
|
||||
Defaults to `false` in all environments.
|
||||
|
||||
* `ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_type`
|
||||
controls what native type Active Record should use when you call `datetime` in
|
||||
a migration or schema. It takes a symbol which must correspond to one of the configured
|
||||
`NATIVE_DATABASE_TYPES`. The default is `:timestamp`, meaning `t.datetime` in
|
||||
a migration will create a "timestamp without time zone" column. To use
|
||||
"timestamp with time zone", change this to `:timestamptz` in an initializer.
|
||||
You should run `bin/rails db:migrate` to rebuild your schema.rb if you change this.
|
||||
|
||||
The schema dumper adds two additional configuration options:
|
||||
|
||||
* `ActiveRecord::SchemaDumper.ignore_tables` accepts an array of tables that should _not_ be included in any generated schema file.
|
||||
|
Loading…
Reference in New Issue
Block a user