Merge pull request #43831 from matthewd/connection-timezone
Allow default_timezone to vary between databases
This commit is contained in:
commit
85f45350e6
|
@ -121,7 +121,7 @@ module ActiveRecord
|
||||||
# if the value is a Time responding to usec.
|
# if the value is a Time responding to usec.
|
||||||
def quoted_date(value)
|
def quoted_date(value)
|
||||||
if value.acts_like?(:time)
|
if value.acts_like?(:time)
|
||||||
if ActiveRecord.default_timezone == :utc
|
if default_timezone == :utc
|
||||||
value = value.getutc if !value.utc?
|
value = value.getutc if !value.utc?
|
||||||
else
|
else
|
||||||
value = value.getlocal
|
value = value.getlocal
|
||||||
|
|
|
@ -62,6 +62,16 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.validate_default_timezone(config)
|
||||||
|
case config
|
||||||
|
when nil
|
||||||
|
when "utc", "local"
|
||||||
|
config.to_sym
|
||||||
|
else
|
||||||
|
raise ArgumentError, "default_timezone must be either 'utc' or 'local'"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
DEFAULT_READ_QUERY = [:begin, :commit, :explain, :release, :rollback, :savepoint, :select, :with] # :nodoc:
|
DEFAULT_READ_QUERY = [:begin, :commit, :explain, :release, :rollback, :savepoint, :select, :with] # :nodoc:
|
||||||
private_constant :DEFAULT_READ_QUERY
|
private_constant :DEFAULT_READ_QUERY
|
||||||
|
|
||||||
|
@ -100,6 +110,8 @@ module ActiveRecord
|
||||||
@advisory_locks_enabled = self.class.type_cast_config_to_boolean(
|
@advisory_locks_enabled = self.class.type_cast_config_to_boolean(
|
||||||
config.fetch(:advisory_locks, true)
|
config.fetch(:advisory_locks, true)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@default_timezone = self.class.validate_default_timezone(config[:default_timezone])
|
||||||
end
|
end
|
||||||
|
|
||||||
EXCEPTION_NEVER = { Exception => :never }.freeze # :nodoc:
|
EXCEPTION_NEVER = { Exception => :never }.freeze # :nodoc:
|
||||||
|
@ -129,6 +141,10 @@ module ActiveRecord
|
||||||
@config.fetch(:use_metadata_table, true)
|
@config.fetch(:use_metadata_table, true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def default_timezone
|
||||||
|
@default_timezone || ActiveRecord.default_timezone
|
||||||
|
end
|
||||||
|
|
||||||
# Determines whether writes are currently being prevented.
|
# Determines whether writes are currently being prevented.
|
||||||
#
|
#
|
||||||
# Returns true if the connection is a replica.
|
# Returns true if the connection is a replica.
|
||||||
|
@ -671,6 +687,21 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
|
def register_class_with_precision(mapping, key, klass, **kwargs) # :nodoc:
|
||||||
|
mapping.register_type(key) do |*args|
|
||||||
|
precision = extract_precision(args.last)
|
||||||
|
klass.new(precision: precision, **kwargs)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def extended_type_map(default_timezone:) # :nodoc:
|
||||||
|
Type::TypeMap.new(self::TYPE_MAP).tap do |m|
|
||||||
|
register_class_with_precision m, %r(\A[^\(]*time)i, Type::Time, timezone: default_timezone
|
||||||
|
register_class_with_precision m, %r(\A[^\(]*datetime)i, Type::DateTime, timezone: default_timezone
|
||||||
|
m.alias_type %r(\A[^\(]*timestamp)i, "datetime"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def initialize_type_map(m)
|
def initialize_type_map(m)
|
||||||
register_class_with_limit m, %r(boolean)i, Type::Boolean
|
register_class_with_limit m, %r(boolean)i, Type::Boolean
|
||||||
|
@ -712,13 +743,6 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def register_class_with_precision(mapping, key, klass)
|
|
||||||
mapping.register_type(key) do |*args|
|
|
||||||
precision = extract_precision(args.last)
|
|
||||||
klass.new(precision: precision)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def extract_scale(sql_type)
|
def extract_scale(sql_type)
|
||||||
case sql_type
|
case sql_type
|
||||||
when /\((\d+)\)/ then 0
|
when /\((\d+)\)/ then 0
|
||||||
|
@ -736,10 +760,23 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
TYPE_MAP = Type::TypeMap.new.tap { |m| initialize_type_map(m) }
|
TYPE_MAP = Type::TypeMap.new.tap { |m| initialize_type_map(m) }
|
||||||
|
EXTENDED_TYPE_MAPS = Concurrent::Map.new
|
||||||
|
|
||||||
private
|
private
|
||||||
|
def extended_type_map_key
|
||||||
|
if @default_timezone
|
||||||
|
{ default_timezone: @default_timezone }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def type_map
|
def type_map
|
||||||
TYPE_MAP
|
if key = extended_type_map_key
|
||||||
|
self.class::EXTENDED_TYPE_MAPS.compute_if_absent(key) do
|
||||||
|
self.class.extended_type_map(**key)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self.class::TYPE_MAP
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def translate_exception_class(e, sql, binds)
|
def translate_exception_class(e, sql, binds)
|
||||||
|
|
|
@ -561,6 +561,14 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
|
def extended_type_map(default_timezone: nil, emulate_booleans:) # :nodoc:
|
||||||
|
super(default_timezone: default_timezone).tap do |m|
|
||||||
|
if emulate_booleans
|
||||||
|
m.register_type %r(^tinyint\(1\))i, Type::Boolean.new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def initialize_type_map(m)
|
def initialize_type_map(m)
|
||||||
super
|
super
|
||||||
|
@ -614,13 +622,16 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
TYPE_MAP = Type::TypeMap.new.tap { |m| initialize_type_map(m) }
|
TYPE_MAP = Type::TypeMap.new.tap { |m| initialize_type_map(m) }
|
||||||
TYPE_MAP_WITH_BOOLEAN = Type::TypeMap.new(TYPE_MAP).tap do |m|
|
EXTENDED_TYPE_MAPS = Concurrent::Map.new
|
||||||
m.register_type %r(^tinyint\(1\))i, Type::Boolean.new
|
EMULATE_BOOLEANS_TRUE = { emulate_booleans: true }.freeze
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
def type_map
|
def extended_type_map_key
|
||||||
emulate_booleans ? TYPE_MAP_WITH_BOOLEAN : TYPE_MAP
|
if @default_timezone
|
||||||
|
{ default_timezone: @default_timezone, emulate_booleans: emulate_booleans }
|
||||||
|
elsif emulate_booleans
|
||||||
|
EMULATE_BOOLEANS_TRUE
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def raw_execute(sql, name, async: false)
|
def raw_execute(sql, name, async: false)
|
||||||
|
|
|
@ -91,7 +91,7 @@ module ActiveRecord
|
||||||
def raw_execute(sql, name, async: false)
|
def raw_execute(sql, name, async: false)
|
||||||
# make sure we carry over any changes to ActiveRecord.default_timezone that have been
|
# make sure we carry over any changes to ActiveRecord.default_timezone that have been
|
||||||
# made since we established the connection
|
# made since we established the connection
|
||||||
@connection.query_options[:database_timezone] = ActiveRecord.default_timezone
|
@connection.query_options[:database_timezone] = default_timezone
|
||||||
|
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
@ -172,7 +172,7 @@ module ActiveRecord
|
||||||
|
|
||||||
# make sure we carry over any changes to ActiveRecord.default_timezone that have been
|
# make sure we carry over any changes to ActiveRecord.default_timezone that have been
|
||||||
# made since we established the connection
|
# made since we established the connection
|
||||||
@connection.query_options[:database_timezone] = ActiveRecord.default_timezone
|
@connection.query_options[:database_timezone] = default_timezone
|
||||||
|
|
||||||
type_casted_binds = type_casted_binds(binds)
|
type_casted_binds = type_casted_binds(binds)
|
||||||
|
|
||||||
|
|
|
@ -57,7 +57,7 @@ module ActiveRecord
|
||||||
# We need to check explicitly for ActiveSupport::TimeWithZone because
|
# We need to check explicitly for ActiveSupport::TimeWithZone because
|
||||||
# we need to transform it to Time objects but we don't want to
|
# we need to transform it to Time objects but we don't want to
|
||||||
# transform Time objects to themselves.
|
# transform Time objects to themselves.
|
||||||
if ActiveRecord.default_timezone == :utc
|
if default_timezone == :utc
|
||||||
value.getutc
|
value.getutc
|
||||||
else
|
else
|
||||||
value.getlocal
|
value.getlocal
|
||||||
|
|
|
@ -575,10 +575,6 @@ module ActiveRecord
|
||||||
m.register_type "polygon", OID::SpecializedString.new(:polygon)
|
m.register_type "polygon", OID::SpecializedString.new(:polygon)
|
||||||
m.register_type "circle", OID::SpecializedString.new(:circle)
|
m.register_type "circle", OID::SpecializedString.new(:circle)
|
||||||
|
|
||||||
register_class_with_precision m, "time", Type::Time
|
|
||||||
register_class_with_precision m, "timestamp", OID::Timestamp
|
|
||||||
register_class_with_precision m, "timestamptz", OID::TimestampWithTimeZone
|
|
||||||
|
|
||||||
m.register_type "numeric" do |_, fmod, sql_type|
|
m.register_type "numeric" do |_, fmod, sql_type|
|
||||||
precision = extract_precision(sql_type)
|
precision = extract_precision(sql_type)
|
||||||
scale = extract_scale(sql_type)
|
scale = extract_scale(sql_type)
|
||||||
|
@ -613,6 +609,11 @@ module ActiveRecord
|
||||||
|
|
||||||
def initialize_type_map(m = type_map)
|
def initialize_type_map(m = type_map)
|
||||||
self.class.initialize_type_map(m)
|
self.class.initialize_type_map(m)
|
||||||
|
|
||||||
|
self.class.register_class_with_precision m, "time", Type::Time, timezone: @default_timezone
|
||||||
|
self.class.register_class_with_precision m, "timestamp", OID::Timestamp, timezone: @default_timezone
|
||||||
|
self.class.register_class_with_precision m, "timestamptz", OID::TimestampWithTimeZone
|
||||||
|
|
||||||
load_additional_types
|
load_additional_types
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -872,7 +873,7 @@ module ActiveRecord
|
||||||
# If using Active Record's time zone support configure the connection to return
|
# If using Active Record's time zone support configure the connection to return
|
||||||
# TIMESTAMP WITH ZONE types in UTC.
|
# TIMESTAMP WITH ZONE types in UTC.
|
||||||
unless variables["timezone"]
|
unless variables["timezone"]
|
||||||
if ActiveRecord.default_timezone == :utc
|
if default_timezone == :utc
|
||||||
variables["timezone"] = "UTC"
|
variables["timezone"] = "UTC"
|
||||||
elsif @local_tz
|
elsif @local_tz
|
||||||
variables["timezone"] = @local_tz
|
variables["timezone"] = @local_tz
|
||||||
|
@ -972,15 +973,15 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_typemap_for_default_timezone
|
def update_typemap_for_default_timezone
|
||||||
if @default_timezone != ActiveRecord.default_timezone && @timestamp_decoder
|
if @mapped_default_timezone != default_timezone && @timestamp_decoder
|
||||||
decoder_class = ActiveRecord.default_timezone == :utc ?
|
decoder_class = default_timezone == :utc ?
|
||||||
PG::TextDecoder::TimestampUtc :
|
PG::TextDecoder::TimestampUtc :
|
||||||
PG::TextDecoder::TimestampWithoutTimeZone
|
PG::TextDecoder::TimestampWithoutTimeZone
|
||||||
|
|
||||||
@timestamp_decoder = decoder_class.new(@timestamp_decoder.to_h)
|
@timestamp_decoder = decoder_class.new(@timestamp_decoder.to_h)
|
||||||
@connection.type_map_for_results.add_coder(@timestamp_decoder)
|
@connection.type_map_for_results.add_coder(@timestamp_decoder)
|
||||||
|
|
||||||
@default_timezone = ActiveRecord.default_timezone
|
@mapped_default_timezone = default_timezone
|
||||||
|
|
||||||
# if default timezone has changed, we need to reconfigure the connection
|
# if default timezone has changed, we need to reconfigure the connection
|
||||||
# (specifically, the session time zone)
|
# (specifically, the session time zone)
|
||||||
|
@ -989,7 +990,7 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_pg_decoders
|
def add_pg_decoders
|
||||||
@default_timezone = nil
|
@mapped_default_timezone = nil
|
||||||
@timestamp_decoder = nil
|
@timestamp_decoder = nil
|
||||||
|
|
||||||
coders_by_name = {
|
coders_by_name = {
|
||||||
|
|
|
@ -370,12 +370,9 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
TYPE_MAP = Type::TypeMap.new.tap { |m| initialize_type_map(m) }
|
TYPE_MAP = Type::TypeMap.new.tap { |m| initialize_type_map(m) }
|
||||||
|
EXTENDED_TYPE_MAPS = Concurrent::Map.new
|
||||||
|
|
||||||
private
|
private
|
||||||
def type_map
|
|
||||||
TYPE_MAP
|
|
||||||
end
|
|
||||||
|
|
||||||
# See https://www.sqlite.org/limits.html,
|
# See https://www.sqlite.org/limits.html,
|
||||||
# the default value is 999 when not configured.
|
# the default value is 999 when not configured.
|
||||||
def bind_params_length
|
def bind_params_length
|
||||||
|
|
|
@ -178,7 +178,7 @@ module ActiveRecord
|
||||||
def can_use_fast_cache_version?(timestamp)
|
def can_use_fast_cache_version?(timestamp)
|
||||||
timestamp.is_a?(String) &&
|
timestamp.is_a?(String) &&
|
||||||
cache_timestamp_format == :usec &&
|
cache_timestamp_format == :usec &&
|
||||||
ActiveRecord.default_timezone == :utc &&
|
self.class.connection.default_timezone == :utc &&
|
||||||
!updated_at_came_from_user?
|
!updated_at_came_from_user?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -75,7 +75,7 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_time_from_proper_timezone
|
def current_time_from_proper_timezone
|
||||||
ActiveRecord.default_timezone == :utc ? Time.now.utc : Time.now
|
connection.default_timezone == :utc ? Time.now.utc : Time.now
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -4,12 +4,17 @@ module ActiveRecord
|
||||||
module Type
|
module Type
|
||||||
module Internal
|
module Internal
|
||||||
module Timezone
|
module Timezone
|
||||||
|
def initialize(timezone: nil, **kwargs)
|
||||||
|
super(**kwargs)
|
||||||
|
@timezone = timezone
|
||||||
|
end
|
||||||
|
|
||||||
def is_utc?
|
def is_utc?
|
||||||
ActiveRecord.default_timezone == :utc
|
default_timezone == :utc
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_timezone
|
def default_timezone
|
||||||
ActiveRecord.default_timezone
|
@timezone || ActiveRecord.default_timezone
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -982,6 +982,48 @@ class BasicsTest < ActiveRecord::TestCase
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
unless in_memory_db?
|
||||||
|
def test_connection_in_local_time
|
||||||
|
with_timezone_config default: :utc do
|
||||||
|
new_config = ActiveRecord::Base.connection_db_config.configuration_hash.merge(default_timezone: "local")
|
||||||
|
ActiveRecord::Base.establish_connection(new_config)
|
||||||
|
Default.reset_column_information
|
||||||
|
|
||||||
|
default = Default.new
|
||||||
|
|
||||||
|
assert_equal Date.new(2004, 1, 1), default.fixed_date
|
||||||
|
assert_equal Time.local(2004, 1, 1, 0, 0, 0, 0), default.fixed_time
|
||||||
|
|
||||||
|
if current_adapter?(:PostgreSQLAdapter)
|
||||||
|
assert_equal Time.utc(2004, 1, 1, 0, 0, 0, 0), default.fixed_time_with_time_zone
|
||||||
|
end
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
ActiveRecord::Base.establish_connection :arunit
|
||||||
|
Default.reset_column_information
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_connection_in_utc_time
|
||||||
|
with_timezone_config default: :local do
|
||||||
|
new_config = ActiveRecord::Base.connection_db_config.configuration_hash.merge(default_timezone: "utc")
|
||||||
|
ActiveRecord::Base.establish_connection(new_config)
|
||||||
|
Default.reset_column_information
|
||||||
|
|
||||||
|
default = Default.new
|
||||||
|
|
||||||
|
assert_equal Date.new(2004, 1, 1), default.fixed_date
|
||||||
|
assert_equal Time.utc(2004, 1, 1, 0, 0, 0, 0), default.fixed_time
|
||||||
|
|
||||||
|
if current_adapter?(:PostgreSQLAdapter)
|
||||||
|
assert_equal Time.utc(2004, 1, 1, 0, 0, 0, 0), default.fixed_time_with_time_zone
|
||||||
|
end
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
ActiveRecord::Base.establish_connection :arunit
|
||||||
|
Default.reset_column_information
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_auto_id
|
def test_auto_id
|
||||||
|
|
|
@ -6,7 +6,13 @@ module ActiveRecord
|
||||||
module ConnectionAdapters
|
module ConnectionAdapters
|
||||||
class QuotingTest < ActiveRecord::TestCase
|
class QuotingTest < ActiveRecord::TestCase
|
||||||
def setup
|
def setup
|
||||||
@quoter = Class.new { include Quoting }.new
|
@quoter = Class.new {
|
||||||
|
include Quoting
|
||||||
|
|
||||||
|
def default_timezone
|
||||||
|
ActiveRecord.default_timezone
|
||||||
|
end
|
||||||
|
}.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_quoted_true
|
def test_quoted_true
|
||||||
|
|
Loading…
Reference in New Issue