1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00
rails--rails/activerecord/test/cases/ar_schema_test.rb
Alex Ghiculescu 867d27dd64 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.
2021-05-06 17:41:55 -05:00

231 lines
7.9 KiB
Ruby

# frozen_string_literal: true
require "cases/helper"
class ActiveRecordSchemaTest < ActiveRecord::TestCase
self.use_transactional_tests = false
setup do
@original_verbose = ActiveRecord::Migration.verbose
ActiveRecord::Migration.verbose = false
@connection = ActiveRecord::Base.connection
@schema_migration = @connection.schema_migration
@schema_migration.drop_table
end
teardown do
@connection.drop_table :fruits rescue nil
@connection.drop_table :nep_fruits rescue nil
@connection.drop_table :nep_schema_migrations rescue nil
@connection.drop_table :has_timestamps rescue nil
@connection.drop_table :multiple_indexes rescue nil
@schema_migration.delete_all rescue nil
ActiveRecord::Migration.verbose = @original_verbose
end
def test_has_primary_key
old_primary_key_prefix_type = ActiveRecord::Base.primary_key_prefix_type
ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore
assert_equal "version", @schema_migration.primary_key
@schema_migration.create_table
assert_difference "@schema_migration.count", 1 do
@schema_migration.create version: 12
end
ensure
@schema_migration.drop_table
ActiveRecord::Base.primary_key_prefix_type = old_primary_key_prefix_type
end
def test_schema_define
ActiveRecord::Schema.define(version: 7) do
create_table :fruits do |t|
t.column :color, :string
t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle
t.column :texture, :string
t.column :flavor, :string
end
end
assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" }
assert_nothing_raised { @connection.select_all "SELECT * FROM schema_migrations" }
assert_equal 7, @connection.migration_context.current_version
end
def test_schema_define_with_table_name_prefix
old_table_name_prefix = ActiveRecord::Base.table_name_prefix
ActiveRecord::Base.table_name_prefix = "nep_"
@schema_migration.reset_table_name
ActiveRecord::InternalMetadata.reset_table_name
ActiveRecord::Schema.define(version: 7) do
create_table :fruits do |t|
t.column :color, :string
t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle
t.column :texture, :string
t.column :flavor, :string
end
end
assert_equal 7, @connection.migration_context.current_version
ensure
ActiveRecord::Base.table_name_prefix = old_table_name_prefix
@schema_migration.reset_table_name
ActiveRecord::InternalMetadata.reset_table_name
end
def test_schema_raises_an_error_for_invalid_column_type
assert_raise NoMethodError do
ActiveRecord::Schema.define(version: 8) do
create_table :vegetables do |t|
t.unknown :color
end
end
end
end
def test_schema_subclass
Class.new(ActiveRecord::Schema).define(version: 9) do
create_table :fruits
end
assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" }
end
def test_normalize_version
assert_equal "118", @schema_migration.normalize_migration_number("0000118")
assert_equal "002", @schema_migration.normalize_migration_number("2")
assert_equal "017", @schema_migration.normalize_migration_number("0017")
assert_equal "20131219224947", @schema_migration.normalize_migration_number("20131219224947")
end
def test_schema_load_with_multiple_indexes_for_column_of_different_names
ActiveRecord::Schema.define do
create_table :multiple_indexes do |t|
t.string "foo"
t.index ["foo"], name: "multiple_indexes_foo_1"
t.index ["foo"], name: "multiple_indexes_foo_2"
end
end
indexes = @connection.indexes("multiple_indexes")
assert_equal 2, indexes.length
assert_equal ["multiple_indexes_foo_1", "multiple_indexes_foo_2"], indexes.collect(&:name).sort
end
if current_adapter?(:PostgreSQLAdapter)
def test_timestamps_with_and_without_zones
ActiveRecord::Schema.define do
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
def test_timestamps_without_null_set_null_to_false_on_create_table
ActiveRecord::Schema.define do
create_table :has_timestamps do |t|
t.timestamps
end
end
assert @connection.column_exists?(:has_timestamps, :created_at, null: false)
assert @connection.column_exists?(:has_timestamps, :updated_at, null: false)
end
def test_timestamps_without_null_set_null_to_false_on_change_table
ActiveRecord::Schema.define do
create_table :has_timestamps
change_table :has_timestamps do |t|
t.timestamps default: Time.now
end
end
assert @connection.column_exists?(:has_timestamps, :created_at, null: false)
assert @connection.column_exists?(:has_timestamps, :updated_at, null: false)
end
if ActiveRecord::Base.connection.supports_bulk_alter?
def test_timestamps_without_null_set_null_to_false_on_change_table_with_bulk
ActiveRecord::Schema.define do
create_table :has_timestamps
change_table :has_timestamps, bulk: true do |t|
t.timestamps default: Time.now
end
end
assert @connection.column_exists?(:has_timestamps, :created_at, null: false)
assert @connection.column_exists?(:has_timestamps, :updated_at, null: false)
end
end
def test_timestamps_without_null_set_null_to_false_on_add_timestamps
ActiveRecord::Schema.define do
create_table :has_timestamps
add_timestamps :has_timestamps, default: Time.now
end
assert @connection.column_exists?(:has_timestamps, :created_at, null: false)
assert @connection.column_exists?(:has_timestamps, :updated_at, null: false)
end
if supports_datetime_with_precision?
def test_timestamps_sets_precision_on_create_table
ActiveRecord::Schema.define do
create_table :has_timestamps do |t|
t.timestamps
end
end
assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false)
assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false)
end
def test_timestamps_sets_precision_on_change_table
ActiveRecord::Schema.define do
create_table :has_timestamps
change_table :has_timestamps do |t|
t.timestamps default: Time.now
end
end
assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false)
assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false)
end
if ActiveRecord::Base.connection.supports_bulk_alter?
def test_timestamps_sets_precision_on_change_table_with_bulk
ActiveRecord::Schema.define do
create_table :has_timestamps
change_table :has_timestamps, bulk: true do |t|
t.timestamps default: Time.now
end
end
assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false)
assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false)
end
end
def test_timestamps_sets_precision_on_add_timestamps
ActiveRecord::Schema.define do
create_table :has_timestamps
add_timestamps :has_timestamps, default: Time.now
end
assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false)
assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false)
end
end
end