Merge pull request #43831 from matthewd/connection-timezone

Allow default_timezone to vary between databases
This commit is contained in:
Matthew Draper 2021-12-13 22:58:59 +10:30 committed by GitHub
commit 85f45350e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 134 additions and 35 deletions

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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 = {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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