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.
|
||||
def quoted_date(value)
|
||||
if value.acts_like?(:time)
|
||||
if ActiveRecord.default_timezone == :utc
|
||||
if default_timezone == :utc
|
||||
value = value.getutc if !value.utc?
|
||||
else
|
||||
value = value.getlocal
|
||||
|
|
|
@ -62,6 +62,16 @@ module ActiveRecord
|
|||
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:
|
||||
private_constant :DEFAULT_READ_QUERY
|
||||
|
||||
|
@ -100,6 +110,8 @@ module ActiveRecord
|
|||
@advisory_locks_enabled = self.class.type_cast_config_to_boolean(
|
||||
config.fetch(:advisory_locks, true)
|
||||
)
|
||||
|
||||
@default_timezone = self.class.validate_default_timezone(config[:default_timezone])
|
||||
end
|
||||
|
||||
EXCEPTION_NEVER = { Exception => :never }.freeze # :nodoc:
|
||||
|
@ -129,6 +141,10 @@ module ActiveRecord
|
|||
@config.fetch(:use_metadata_table, true)
|
||||
end
|
||||
|
||||
def default_timezone
|
||||
@default_timezone || ActiveRecord.default_timezone
|
||||
end
|
||||
|
||||
# Determines whether writes are currently being prevented.
|
||||
#
|
||||
# Returns true if the connection is a replica.
|
||||
|
@ -671,6 +687,21 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
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
|
||||
def initialize_type_map(m)
|
||||
register_class_with_limit m, %r(boolean)i, Type::Boolean
|
||||
|
@ -712,13 +743,6 @@ module ActiveRecord
|
|||
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)
|
||||
case sql_type
|
||||
when /\((\d+)\)/ then 0
|
||||
|
@ -736,10 +760,23 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
TYPE_MAP = Type::TypeMap.new.tap { |m| initialize_type_map(m) }
|
||||
EXTENDED_TYPE_MAPS = Concurrent::Map.new
|
||||
|
||||
private
|
||||
def extended_type_map_key
|
||||
if @default_timezone
|
||||
{ default_timezone: @default_timezone }
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def translate_exception_class(e, sql, binds)
|
||||
|
|
|
@ -561,6 +561,14 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
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
|
||||
def initialize_type_map(m)
|
||||
super
|
||||
|
@ -614,13 +622,16 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
TYPE_MAP = Type::TypeMap.new.tap { |m| initialize_type_map(m) }
|
||||
TYPE_MAP_WITH_BOOLEAN = Type::TypeMap.new(TYPE_MAP).tap do |m|
|
||||
m.register_type %r(^tinyint\(1\))i, Type::Boolean.new
|
||||
end
|
||||
EXTENDED_TYPE_MAPS = Concurrent::Map.new
|
||||
EMULATE_BOOLEANS_TRUE = { emulate_booleans: true }.freeze
|
||||
|
||||
private
|
||||
def type_map
|
||||
emulate_booleans ? TYPE_MAP_WITH_BOOLEAN : TYPE_MAP
|
||||
def extended_type_map_key
|
||||
if @default_timezone
|
||||
{ default_timezone: @default_timezone, emulate_booleans: emulate_booleans }
|
||||
elsif emulate_booleans
|
||||
EMULATE_BOOLEANS_TRUE
|
||||
end
|
||||
end
|
||||
|
||||
def raw_execute(sql, name, async: false)
|
||||
|
|
|
@ -91,7 +91,7 @@ module ActiveRecord
|
|||
def raw_execute(sql, name, async: false)
|
||||
# make sure we carry over any changes to ActiveRecord.default_timezone that have been
|
||||
# made since we established the connection
|
||||
@connection.query_options[:database_timezone] = ActiveRecord.default_timezone
|
||||
@connection.query_options[:database_timezone] = default_timezone
|
||||
|
||||
super
|
||||
end
|
||||
|
@ -172,7 +172,7 @@ module ActiveRecord
|
|||
|
||||
# make sure we carry over any changes to ActiveRecord.default_timezone that have been
|
||||
# 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)
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ module ActiveRecord
|
|||
# We need to check explicitly for ActiveSupport::TimeWithZone because
|
||||
# we need to transform it to Time objects but we don't want to
|
||||
# transform Time objects to themselves.
|
||||
if ActiveRecord.default_timezone == :utc
|
||||
if default_timezone == :utc
|
||||
value.getutc
|
||||
else
|
||||
value.getlocal
|
||||
|
|
|
@ -575,10 +575,6 @@ module ActiveRecord
|
|||
m.register_type "polygon", OID::SpecializedString.new(:polygon)
|
||||
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|
|
||||
precision = extract_precision(sql_type)
|
||||
scale = extract_scale(sql_type)
|
||||
|
@ -613,6 +609,11 @@ module ActiveRecord
|
|||
|
||||
def initialize_type_map(m = type_map)
|
||||
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
|
||||
end
|
||||
|
||||
|
@ -872,7 +873,7 @@ module ActiveRecord
|
|||
# If using Active Record's time zone support configure the connection to return
|
||||
# TIMESTAMP WITH ZONE types in UTC.
|
||||
unless variables["timezone"]
|
||||
if ActiveRecord.default_timezone == :utc
|
||||
if default_timezone == :utc
|
||||
variables["timezone"] = "UTC"
|
||||
elsif @local_tz
|
||||
variables["timezone"] = @local_tz
|
||||
|
@ -972,15 +973,15 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def update_typemap_for_default_timezone
|
||||
if @default_timezone != ActiveRecord.default_timezone && @timestamp_decoder
|
||||
decoder_class = ActiveRecord.default_timezone == :utc ?
|
||||
if @mapped_default_timezone != default_timezone && @timestamp_decoder
|
||||
decoder_class = default_timezone == :utc ?
|
||||
PG::TextDecoder::TimestampUtc :
|
||||
PG::TextDecoder::TimestampWithoutTimeZone
|
||||
|
||||
@timestamp_decoder = decoder_class.new(@timestamp_decoder.to_h)
|
||||
@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
|
||||
# (specifically, the session time zone)
|
||||
|
@ -989,7 +990,7 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def add_pg_decoders
|
||||
@default_timezone = nil
|
||||
@mapped_default_timezone = nil
|
||||
@timestamp_decoder = nil
|
||||
|
||||
coders_by_name = {
|
||||
|
|
|
@ -370,12 +370,9 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
TYPE_MAP = Type::TypeMap.new.tap { |m| initialize_type_map(m) }
|
||||
EXTENDED_TYPE_MAPS = Concurrent::Map.new
|
||||
|
||||
private
|
||||
def type_map
|
||||
TYPE_MAP
|
||||
end
|
||||
|
||||
# See https://www.sqlite.org/limits.html,
|
||||
# the default value is 999 when not configured.
|
||||
def bind_params_length
|
||||
|
|
|
@ -178,7 +178,7 @@ module ActiveRecord
|
|||
def can_use_fast_cache_version?(timestamp)
|
||||
timestamp.is_a?(String) &&
|
||||
cache_timestamp_format == :usec &&
|
||||
ActiveRecord.default_timezone == :utc &&
|
||||
self.class.connection.default_timezone == :utc &&
|
||||
!updated_at_came_from_user?
|
||||
end
|
||||
|
||||
|
|
|
@ -75,7 +75,7 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
private
|
||||
|
|
|
@ -4,12 +4,17 @@ module ActiveRecord
|
|||
module Type
|
||||
module Internal
|
||||
module Timezone
|
||||
def initialize(timezone: nil, **kwargs)
|
||||
super(**kwargs)
|
||||
@timezone = timezone
|
||||
end
|
||||
|
||||
def is_utc?
|
||||
ActiveRecord.default_timezone == :utc
|
||||
default_timezone == :utc
|
||||
end
|
||||
|
||||
def default_timezone
|
||||
ActiveRecord.default_timezone
|
||||
@timezone || ActiveRecord.default_timezone
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -982,6 +982,48 @@ class BasicsTest < ActiveRecord::TestCase
|
|||
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
|
||||
|
||||
def test_auto_id
|
||||
|
|
|
@ -6,7 +6,13 @@ module ActiveRecord
|
|||
module ConnectionAdapters
|
||||
class QuotingTest < ActiveRecord::TestCase
|
||||
def setup
|
||||
@quoter = Class.new { include Quoting }.new
|
||||
@quoter = Class.new {
|
||||
include Quoting
|
||||
|
||||
def default_timezone
|
||||
ActiveRecord.default_timezone
|
||||
end
|
||||
}.new
|
||||
end
|
||||
|
||||
def test_quoted_true
|
||||
|
|
Loading…
Reference in New Issue