activerecord-connection-tz 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +110 -0
  5. data/lib/active_record_connection_tz/adapters/mysql2.rb +28 -0
  6. data/lib/active_record_connection_tz/adapters/shared.rb +94 -0
  7. data/lib/active_record_connection_tz/adapters/trilogy.rb +28 -0
  8. data/lib/active_record_connection_tz/connection_settings.rb +132 -0
  9. data/lib/active_record_connection_tz/temporal_formatter.rb +22 -0
  10. data/lib/active_record_connection_tz/temporal_parser.rb +25 -0
  11. data/lib/active_record_connection_tz/types/zoned_date_time.rb +31 -0
  12. data/lib/active_record_connection_tz/types/zoned_temporal.rb +54 -0
  13. data/lib/active_record_connection_tz/types/zoned_time.rb +28 -0
  14. data/lib/active_record_connection_tz/types.rb +18 -0
  15. data/lib/active_record_connection_tz/version.rb +5 -0
  16. data/lib/active_record_connection_tz.rb +43 -0
  17. data/lib/activerecord-connection-tz.rb +3 -0
  18. data/sig/active_record_connection_tz/adapters.rbs +34 -0
  19. data/sig/generated/active_record_connection_tz/adapters/mysql2.rbs +17 -0
  20. data/sig/generated/active_record_connection_tz/adapters/shared.rbs +37 -0
  21. data/sig/generated/active_record_connection_tz/adapters/trilogy.rbs +17 -0
  22. data/sig/generated/active_record_connection_tz/connection_settings.rbs +53 -0
  23. data/sig/generated/active_record_connection_tz/temporal_formatter.rbs +11 -0
  24. data/sig/generated/active_record_connection_tz/temporal_parser.rbs +14 -0
  25. data/sig/generated/active_record_connection_tz/types/zoned_date_time.rbs +20 -0
  26. data/sig/generated/active_record_connection_tz/types/zoned_temporal.rbs +33 -0
  27. data/sig/generated/active_record_connection_tz/types/zoned_time.rbs +20 -0
  28. data/sig/generated/active_record_connection_tz/types.rbs +8 -0
  29. data/sig/generated/active_record_connection_tz/version.rbs +5 -0
  30. data/sig/generated/active_record_connection_tz.rbs +17 -0
  31. data/sig/generated/activerecord-connection-tz.rbs +2 -0
  32. metadata +97 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d5ef49ac42955d476269f8702a521271591abd43f0dea2b7dfbd0c5d168e855d
4
+ data.tar.gz: 2f8d972eb058c16c06ba1ed3851e366f2758047444d151af0664644ae9375455
5
+ SHA512:
6
+ metadata.gz: ba880a8c2ed7274946327afd0087e1f50aa4454fe217547d3cc1256884fdbe7135f84c0805b55b814d543cc7d72ef9b9d669ef6b4e5a1bcc4c5a074704ac1f4e
7
+ data.tar.gz: 03c838ee16bbaa223403a5bd0c7d58c4e44104b19cc16d9ac64ed17aae4ffe21c97a12c5403ada924ffb658e5c07126af9a94ed5fba8dd5d0789a4ee53f91178
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # CHANGELOG
2
+
3
+ ## [0.1.0] - 2026-05-10
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 shoma07
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # ActiveRecord Connection TZ
2
+
3
+ > ⚠️ **Early stage (v0.1.0):** This gem is still being refined. APIs, configuration details, and supported surfaces may change before the first stable release.
4
+
5
+ ## Overview
6
+
7
+ `activerecord-connection-tz` helps Rails applications use **mixed-timezone MySQL connections** in the same process.
8
+
9
+ It is designed for cases where one connection follows a UTC contract while another connection needs temporal values to be interpreted with a different configured timezone.
10
+
11
+ The gem applies connection-scoped handling through `database.yml`, so the tagged connection can:
12
+
13
+ - set the MySQL session `time_zone`
14
+ - replace `datetime` / `time` / `timestamp` casting on a per-connection basis
15
+ - preserve fractional-second precision when writing temporal values
16
+ - work with both `mysql2` and `trilogy`
17
+
18
+ ## Installation
19
+
20
+ Add the gem together with the adapter gem you use:
21
+
22
+ ```ruby
23
+ # Gemfile
24
+ gem 'activerecord-connection-tz'
25
+ gem 'mysql2' # or gem 'trilogy'
26
+ ```
27
+
28
+ And then execute:
29
+
30
+ ```sh
31
+ bundle install
32
+ ```
33
+
34
+ ## Quick start
35
+
36
+ The gem activates automatically for connections that declare `connection_tz` in `database.yml`.
37
+
38
+ ```yml
39
+ production:
40
+ primary:
41
+ adapter: mysql2
42
+ database: primary_db
43
+
44
+ legacy_tokyo:
45
+ adapter: mysql2
46
+ database: legacy_db
47
+ connection_tz:
48
+ time_zone: Asia/Tokyo
49
+ ```
50
+
51
+ No model-level or attribute-level wiring is required for the tagged connection.
52
+
53
+ For transition-free zones such as `Asia/Tokyo`, the gem can derive the MySQL session `time_zone` automatically from `time_zone`.
54
+
55
+ For zones with DST or other offset transitions, set `mysql_session_time_zone` explicitly:
56
+
57
+ ```yml
58
+ production:
59
+ legacy_new_york:
60
+ adapter: mysql2
61
+ database: legacy_db
62
+ connection_tz:
63
+ time_zone: America/New_York
64
+ mysql_session_time_zone: America/New_York
65
+ ```
66
+
67
+ If those DST-style zones use `TIMESTAMP` columns, prefer a **named MySQL timezone** such as `America/New_York` instead of a fixed offset such as `-05:00`. A fixed offset can skew `TIMESTAMP` reads across DST boundaries because MySQL converts `TIMESTAMP` values through the session timezone, while `DATETIME` values are not affected by that limitation.
68
+
69
+ ## Documentation
70
+
71
+ For runnable Rails examples and setup notes, see **[examples/README.md](examples/README.md)**.
72
+
73
+ The examples cover:
74
+
75
+ - Rails 7.1 / 8.0 / 8.1
76
+ - mysql2 and trilogy side by side
77
+ - shared MySQL 8 Docker setup
78
+ - UTC `primary` plus a tagged connection interpreted with another timezone
79
+
80
+ ## Requirements
81
+
82
+ - Ruby 3.2+
83
+ - Active Record 7.1 - 8.1
84
+ - MySQL 5.7+ (or a compatible server with fractional-second precision support)
85
+ - `mysql2` or `trilogy`
86
+
87
+ ## Development
88
+
89
+ ```sh
90
+ bundle install
91
+ bundle exec rbs collection install
92
+ bundle exec rspec
93
+ bundle exec rubocop
94
+ bundle exec rake sig:generate
95
+ bundle exec rake typecheck
96
+ ```
97
+
98
+ To build the gem locally:
99
+
100
+ ```sh
101
+ gem build activerecord-connection-tz.gemspec
102
+ ```
103
+
104
+ ## Contributing
105
+
106
+ Bug reports and pull requests are welcome on GitHub at https://github.com/shoma07/activerecord-connection-tz.
107
+
108
+ ## License
109
+
110
+ The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/lazy_load_hooks'
4
+
5
+ module ActiveRecordConnectionTz
6
+ module Adapters
7
+ module Mysql2
8
+ class << self
9
+ #: () -> void
10
+ def install!
11
+ Shared.install!(adapter_module: self, hook_name: :active_record_mysql2adapter)
12
+ end
13
+ end
14
+
15
+ module InstanceMethods
16
+ private
17
+
18
+ #: () -> void
19
+ def disable_temporal_cast
20
+ # Use the adapter's public accessor even though it marks the raw
21
+ # connection as touched; this runs during configure_connection and
22
+ # avoids reaching into the adapter's ivar directly.
23
+ raw_connection.query_options[:cast] = false
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/lazy_load_hooks'
4
+
5
+ module ActiveRecordConnectionTz
6
+ module Adapters
7
+ module Shared
8
+ class << self
9
+ #: (adapter_module: Module, hook_name: Symbol) -> void
10
+ def install!(adapter_module:, hook_name:)
11
+ return if adapter_module.instance_variable_get(:@installed)
12
+
13
+ instance_methods = adapter_module.const_get(:InstanceMethods, false)
14
+
15
+ ActiveSupport.on_load(hook_name) do
16
+ singleton_class.prepend(Shared::ClassMethods) unless singleton_class < Shared::ClassMethods
17
+ prepend(Shared::InstanceMethods) unless self < Shared::InstanceMethods
18
+ prepend(instance_methods) unless self < instance_methods
19
+ end
20
+
21
+ adapter_module.instance_variable_set(:@installed, true)
22
+ end
23
+ end
24
+
25
+ module ClassMethods
26
+ #: (emulate_booleans: bool, ?default_timezone: untyped, ?connection_tz: Hash[untyped, untyped]?) -> ActiveRecord::Type::TypeMap
27
+ def extended_type_map(emulate_booleans:, default_timezone: nil, connection_tz: nil)
28
+ super(default_timezone: default_timezone, emulate_booleans: emulate_booleans).tap do |map|
29
+ next unless connection_tz
30
+
31
+ settings = ConnectionSettings.new(connection_tz)
32
+ time_zone = settings.time_zone || raise(ConfigurationError, 'connection_tz time_zone must be present')
33
+ Types.register_mysql_temporal_types!(map, time_zone: time_zone)
34
+ end
35
+ end
36
+ end
37
+
38
+ module InstanceMethods
39
+ private
40
+
41
+ #: () -> untyped
42
+ def configure_connection
43
+ settings = connection_tz_settings
44
+ @config = settings.apply_mysql_variables(@config)
45
+ super
46
+
47
+ disable_temporal_cast if settings.enabled?
48
+ end
49
+
50
+ #: () -> untyped
51
+ def extended_type_map_key
52
+ settings = connection_tz_settings
53
+ return super unless settings.enabled?
54
+
55
+ (super || { emulate_booleans: emulate_booleans }).merge(connection_tz: settings.type_map_key)
56
+ end
57
+
58
+ #: (untyped value) -> untyped
59
+ def quoted_date(value)
60
+ settings = connection_tz_settings
61
+ return super unless settings.enabled? && value.acts_like?(:time)
62
+
63
+ # `enabled?` guarantees the configured timezone is present for this connection.
64
+ # The `|| raise` keeps Steep narrowed from `ActiveSupport::TimeZone?`.
65
+ time_zone = settings.time_zone || raise(ConfigurationError, 'connection_tz time_zone must be present')
66
+ TemporalFormatter.quoted_date(value, time_zone: time_zone)
67
+ end
68
+
69
+ #: (untyped value) -> untyped
70
+ def quoted_time(value)
71
+ settings = connection_tz_settings
72
+ return super unless settings.enabled? && value.acts_like?(:time)
73
+
74
+ # `enabled?` guarantees the configured timezone is present for this connection.
75
+ # The `|| raise` keeps Steep narrowed from `ActiveSupport::TimeZone?`.
76
+ time_zone = settings.time_zone || raise(ConfigurationError, 'connection_tz time_zone must be present')
77
+ TemporalFormatter.quoted_time(value, time_zone: time_zone)
78
+ end
79
+
80
+ #: () -> ConnectionSettings
81
+ def connection_tz_settings
82
+ # Active Record keeps the connection configuration in `@config`
83
+ # without exposing a stable reader on the adapter instance.
84
+ @connection_tz_settings ||= ConnectionSettings.from_config(@config)
85
+ end
86
+
87
+ #: () -> void
88
+ def disable_temporal_cast
89
+ raise NotImplementedError, "#{self.class} must implement #disable_temporal_cast"
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/lazy_load_hooks'
4
+
5
+ module ActiveRecordConnectionTz
6
+ module Adapters
7
+ module Trilogy
8
+ class << self
9
+ #: () -> void
10
+ def install!
11
+ Shared.install!(adapter_module: self, hook_name: :active_record_trilogyadapter)
12
+ end
13
+ end
14
+
15
+ module InstanceMethods
16
+ private
17
+
18
+ #: () -> void
19
+ def disable_temporal_cast
20
+ # Use the adapter's public accessor even though it marks the raw
21
+ # connection as touched; this runs during configure_connection and
22
+ # avoids reaching into the adapter's ivar directly.
23
+ raw_connection.query_flags &= ~::Trilogy::QUERY_FLAGS_CAST
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/time'
4
+
5
+ module ActiveRecordConnectionTz
6
+ class ConnectionSettings
7
+ KEY = 'connection_tz' #: String
8
+ private_constant :KEY
9
+
10
+ # Range used to detect whether a timezone stays fixed-offset for the practical
11
+ # lifetime of MySQL TIMESTAMP data, starting at the Unix epoch and extending
12
+ # slightly beyond 2038 to give the transition check some future slack.
13
+ AUTO_SESSION_TIME_ZONE_FROM = Time.utc(1970, 1, 1).freeze #: Time
14
+ private_constant :AUTO_SESSION_TIME_ZONE_FROM
15
+
16
+ AUTO_SESSION_TIME_ZONE_TO = Time.utc(2050, 1, 1).freeze #: Time
17
+ private_constant :AUTO_SESSION_TIME_ZONE_TO
18
+
19
+ attr_reader :time_zone_name #: String?
20
+ attr_reader :explicit_mysql_session_time_zone #: String?
21
+
22
+ class << self
23
+ #: (Hash[untyped, untyped] config) -> ConnectionSettings
24
+ def from_config(config)
25
+ new(config[KEY] || config[KEY.to_sym])
26
+ end
27
+ end
28
+
29
+ #: (Hash[untyped, untyped]? raw) -> void
30
+ def initialize(raw)
31
+ raw = normalize_hash(raw)
32
+
33
+ @time_zone_name = raw.fetch('time_zone', nil)
34
+ @explicit_mysql_session_time_zone = raw.fetch('mysql_session_time_zone', nil)
35
+ end
36
+
37
+ #: () -> bool
38
+ def enabled?
39
+ !time_zone_name.to_s.empty?
40
+ end
41
+
42
+ #: () -> ActiveSupport::TimeZone?
43
+ def time_zone
44
+ return unless enabled?
45
+
46
+ # `enabled?` guarantees `time_zone_name` is present here.
47
+ # The `|| raise` keeps Steep's type narrowed from `String?` to `String`.
48
+ name = time_zone_name || raise(ConfigurationError, 'connection_tz time_zone must be present')
49
+
50
+ @time_zone ||= ActiveSupport::TimeZone[name] || raise(
51
+ ConfigurationError,
52
+ "Unknown connection_tz time_zone: #{name.inspect}"
53
+ )
54
+ end
55
+
56
+ #: () -> Hash[String, String]?
57
+ def type_map_key
58
+ return unless enabled?
59
+
60
+ # `enabled?` guarantees `time_zone_name` is present here.
61
+ # The `|| raise` keeps Steep's type narrowed from `String?` to `String`.
62
+ name = time_zone_name || raise(ConfigurationError, 'connection_tz time_zone must be present')
63
+
64
+ {
65
+ 'time_zone' => name
66
+ }
67
+ end
68
+
69
+ #: () -> String?
70
+ def mysql_session_time_zone
71
+ explicit_mysql_session_time_zone || auto_mysql_session_time_zone
72
+ end
73
+
74
+ #: (Hash[untyped, untyped] config) -> Hash[untyped, untyped]
75
+ def apply_mysql_variables(config)
76
+ return config unless enabled?
77
+
78
+ variables = normalize_hash(config['variables'] || config[:variables], context: 'connection_tz.variables')
79
+ return config if variables['time_zone']
80
+
81
+ session_time_zone = mysql_session_time_zone || raise(
82
+ ConfigurationError,
83
+ "connection_tz time_zone #{time_zone_name.inspect} is not fixed-offset " \
84
+ 'between 1970 and 2050; set mysql_session_time_zone explicitly'
85
+ )
86
+
87
+ variables['time_zone'] = session_time_zone
88
+
89
+ config.merge(variables: variables.dup, 'variables' => variables.dup)
90
+ end
91
+
92
+ private
93
+
94
+ #: () -> String?
95
+ def auto_mysql_session_time_zone
96
+ return unless fixed_offset_time_zone?
97
+
98
+ # `fixed_offset_time_zone?` already confirms the setting is enabled.
99
+ # Keep the `|| raise` so Steep narrows `ActiveSupport::TimeZone?`.
100
+ zone = time_zone || raise(ConfigurationError, 'connection_tz time_zone must be present')
101
+
102
+ format_utc_offset(zone.utc_offset)
103
+ end
104
+
105
+ #: () -> bool
106
+ def fixed_offset_time_zone?
107
+ zone = time_zone
108
+ return false unless zone
109
+
110
+ zone.tzinfo.transitions_up_to(AUTO_SESSION_TIME_ZONE_TO, AUTO_SESSION_TIME_ZONE_FROM).empty?
111
+ end
112
+
113
+ #: (Integer offset_seconds) -> String
114
+ def format_utc_offset(offset_seconds)
115
+ sign = offset_seconds.negative? ? '-' : '+'
116
+ hours, remainder = offset_seconds.abs.divmod(3600)
117
+
118
+ format('%<sign>s%<hours>02d:%<minutes>02d', sign:, hours:, minutes: remainder / 60)
119
+ end
120
+
121
+ #: (untyped value, ?context: String) -> Hash[String, untyped]
122
+ def normalize_hash(value, context: 'connection_tz configuration')
123
+ return {} if value.nil?
124
+ unless value.is_a?(Hash)
125
+ raise ConfigurationError,
126
+ "#{context} must be a Hash, got #{value.class} (#{value.inspect})"
127
+ end
128
+
129
+ value.transform_keys(&:to_s)
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordConnectionTz
4
+ module TemporalFormatter
5
+ class << self
6
+ #: (Time | DateTime | ActiveSupport::TimeWithZone value, time_zone: ActiveSupport::TimeZone) -> String
7
+ def quoted_date(value, time_zone:)
8
+ zoned = value.in_time_zone(time_zone)
9
+ result = zoned.strftime('%Y-%m-%d %H:%M:%S')
10
+ return "#{result}.#{format('%06d', zoned.usec)}" if zoned.usec.positive?
11
+
12
+ result
13
+ end
14
+
15
+ #: (Time | DateTime | ActiveSupport::TimeWithZone value, time_zone: ActiveSupport::TimeZone) -> String
16
+ def quoted_time(value, time_zone:)
17
+ # Use the same stable anchor date ActiveRecord::Type::Time conventionally relies on for TIME values.
18
+ quoted_date(value.change(year: 2000, month: 1, day: 1), time_zone: time_zone).sub(/\A\d{4}-\d{2}-\d{2} /, '')
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordConnectionTz
4
+ module TemporalParser
5
+ # Treat MySQL year-zero and zero/partial-zero dates as nil instead of passing invalid dates to TimeZone parsing.
6
+ ZERO_DATETIME = /\A(?:0000-\d{2}-\d{2}|\d{4}-(?:00-\d{2}|\d{2}-00))/ #: Regexp
7
+ private_constant :ZERO_DATETIME
8
+
9
+ class << self
10
+ #: (String? value, time_zone: ActiveSupport::TimeZone) -> ActiveSupport::TimeWithZone?
11
+ def parse_datetime(value, time_zone:)
12
+ return if value.nil? || value == '' || value.match?(ZERO_DATETIME)
13
+
14
+ time_zone.parse(value)
15
+ end
16
+
17
+ #: (String? value, time_zone: ActiveSupport::TimeZone) -> ActiveSupport::TimeWithZone?
18
+ def parse_time(value, time_zone:)
19
+ return if value.nil? || value == ''
20
+
21
+ time_zone.parse("2000-01-01 #{value}")
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordConnectionTz
4
+ module Types
5
+ class ZonedDateTime < ActiveRecord::Type::DateTime
6
+ include ZonedTemporal
7
+
8
+ #: (untyped value) -> untyped
9
+ def cast(value)
10
+ # ActiveRecord::Type::DateTime#cast converts TimeWithZone to DateTime,
11
+ # dropping the original zone. Keep already-zoned values intact.
12
+ return value if value.is_a?(ActiveSupport::TimeWithZone)
13
+ return super unless value.is_a?(String)
14
+
15
+ parse_string(value)
16
+ end
17
+
18
+ private
19
+
20
+ #: (String value) -> ActiveSupport::TimeWithZone?
21
+ def parse_string(value)
22
+ TemporalParser.parse_datetime(value, time_zone: time_zone)
23
+ end
24
+
25
+ #: (untyped casted) -> String
26
+ def quote_casted(casted)
27
+ TemporalFormatter.quoted_date(casted, time_zone: time_zone)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordConnectionTz
4
+ module Types
5
+ # @rbs module-self ActiveModel::Type::Value
6
+ module ZonedTemporal
7
+ attr_reader :time_zone #: ActiveSupport::TimeZone
8
+
9
+ #: (time_zone: ActiveSupport::TimeZone) -> void
10
+ def initialize(time_zone:)
11
+ super()
12
+ @time_zone = time_zone
13
+ end
14
+
15
+ #: (untyped value) -> untyped
16
+ def deserialize(value)
17
+ return super unless value.is_a?(String)
18
+
19
+ parse_string(value)
20
+ end
21
+
22
+ #: (untyped value) -> untyped
23
+ def serialize(value)
24
+ serialize_temporal(value)
25
+ end
26
+
27
+ #: (untyped value) -> untyped
28
+ def serialize_cast_value(value) # :nodoc:
29
+ serialize_temporal(value)
30
+ end
31
+
32
+ private
33
+
34
+ #: (untyped value) -> untyped
35
+ def serialize_temporal(value)
36
+ casted = cast(value)
37
+ return if casted.nil?
38
+ return casted unless casted.acts_like?(:time)
39
+
40
+ quote_casted(casted)
41
+ end
42
+
43
+ #: (String _value) -> untyped
44
+ def parse_string(_value)
45
+ raise NotImplementedError, "#{self.class} must implement #parse_string"
46
+ end
47
+
48
+ #: (untyped _value) -> String
49
+ def quote_casted(_value)
50
+ raise NotImplementedError, "#{self.class} must implement #quote_casted"
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordConnectionTz
4
+ module Types
5
+ class ZonedTime < ActiveRecord::Type::Time
6
+ include ZonedTemporal
7
+
8
+ #: (untyped value) -> untyped
9
+ def cast(value)
10
+ return super unless value.is_a?(String)
11
+
12
+ parse_string(value)
13
+ end
14
+
15
+ private
16
+
17
+ #: (String value) -> ActiveSupport::TimeWithZone?
18
+ def parse_string(value)
19
+ TemporalParser.parse_time(value, time_zone: time_zone)
20
+ end
21
+
22
+ #: (untyped casted) -> String
23
+ def quote_casted(casted)
24
+ TemporalFormatter.quoted_time(casted, time_zone: time_zone)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'types/zoned_temporal'
4
+ require_relative 'types/zoned_date_time'
5
+ require_relative 'types/zoned_time'
6
+
7
+ module ActiveRecordConnectionTz
8
+ module Types
9
+ class << self
10
+ #: (ActiveRecord::Type::TypeMap map, time_zone: ActiveSupport::TimeZone) -> void
11
+ def register_mysql_temporal_types!(map, time_zone:)
12
+ map.register_type(/\Adatetime\b/i) { |_sql_type| ZonedDateTime.new(time_zone: time_zone) }
13
+ map.register_type(/\Atime\b/i) { |_sql_type| ZonedTime.new(time_zone: time_zone) }
14
+ map.alias_type(/\Atimestamp\b/i, 'datetime')
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordConnectionTz
4
+ VERSION = '0.1.0' #: String
5
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_support/lazy_load_hooks'
5
+ require 'active_record'
6
+
7
+ require_relative 'active_record_connection_tz/version'
8
+ require_relative 'active_record_connection_tz/connection_settings'
9
+ require_relative 'active_record_connection_tz/temporal_formatter'
10
+ require_relative 'active_record_connection_tz/temporal_parser'
11
+ require_relative 'active_record_connection_tz/types'
12
+ require_relative 'active_record_connection_tz/adapters/shared'
13
+ require_relative 'active_record_connection_tz/adapters/mysql2'
14
+ require_relative 'active_record_connection_tz/adapters/trilogy'
15
+
16
+ module ActiveRecordConnectionTz
17
+ class Error < StandardError; end
18
+ class ConfigurationError < Error; end
19
+ INSTALL_MUTEX = Mutex.new #: Mutex
20
+ private_constant :INSTALL_MUTEX
21
+
22
+ class << self
23
+ #: () -> void
24
+ def install!
25
+ return if @installed
26
+
27
+ INSTALL_MUTEX.synchronize do
28
+ return if @installed
29
+
30
+ Adapters::Mysql2.install!
31
+ Adapters::Trilogy.install!
32
+ @installed = true
33
+ end
34
+ end
35
+
36
+ #: () -> bool
37
+ def installed?
38
+ @installed == true
39
+ end
40
+ end
41
+ end
42
+
43
+ ActiveRecordConnectionTz.install!
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'active_record_connection_tz'
@@ -0,0 +1,34 @@
1
+ module ActiveRecordConnectionTz
2
+ module Adapters
3
+ interface _SharedClassMethodsHost
4
+ def extended_type_map: (emulate_booleans: bool, ?default_timezone: untyped, ?connection_tz: Hash[untyped, untyped]?) -> ActiveRecord::Type::TypeMap
5
+ end
6
+
7
+ interface _SharedInstanceMethodsHost
8
+ def config: () -> Hash[untyped, untyped]
9
+ def configure_connection: () -> untyped
10
+ def extended_type_map_key: () -> untyped
11
+ def quoted_date: (untyped value) -> untyped
12
+ def quoted_time: (untyped value) -> untyped
13
+ def emulate_booleans: () -> bool
14
+ end
15
+
16
+ module Shared
17
+ module ClassMethods : _SharedClassMethodsHost
18
+ end
19
+
20
+ module InstanceMethods : _SharedInstanceMethodsHost
21
+ end
22
+ end
23
+
24
+ module Mysql2
25
+ module InstanceMethods : ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter
26
+ end
27
+ end
28
+
29
+ module Trilogy
30
+ module InstanceMethods : ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,17 @@
1
+ # Generated from lib/active_record_connection_tz/adapters/mysql2.rb with RBS::Inline
2
+
3
+ module ActiveRecordConnectionTz
4
+ module Adapters
5
+ module Mysql2
6
+ # : () -> void
7
+ def self.install!: () -> void
8
+
9
+ module InstanceMethods
10
+ private
11
+
12
+ # : () -> void
13
+ def disable_temporal_cast: () -> void
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,37 @@
1
+ # Generated from lib/active_record_connection_tz/adapters/shared.rb with RBS::Inline
2
+
3
+ module ActiveRecordConnectionTz
4
+ module Adapters
5
+ module Shared
6
+ # : (adapter_module: Module, hook_name: Symbol) -> void
7
+ def self.install!: (adapter_module: Module, hook_name: Symbol) -> void
8
+
9
+ module ClassMethods
10
+ # : (emulate_booleans: bool, ?default_timezone: untyped, ?connection_tz: Hash[untyped, untyped]?) -> ActiveRecord::Type::TypeMap
11
+ def extended_type_map: (emulate_booleans: bool, ?default_timezone: untyped, ?connection_tz: Hash[untyped, untyped]?) -> ActiveRecord::Type::TypeMap
12
+ end
13
+
14
+ module InstanceMethods
15
+ private
16
+
17
+ # : () -> untyped
18
+ def configure_connection: () -> untyped
19
+
20
+ # : () -> untyped
21
+ def extended_type_map_key: () -> untyped
22
+
23
+ # : (untyped value) -> untyped
24
+ def quoted_date: (untyped value) -> untyped
25
+
26
+ # : (untyped value) -> untyped
27
+ def quoted_time: (untyped value) -> untyped
28
+
29
+ # : () -> ConnectionSettings
30
+ def connection_tz_settings: () -> ConnectionSettings
31
+
32
+ # : () -> void
33
+ def disable_temporal_cast: () -> void
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,17 @@
1
+ # Generated from lib/active_record_connection_tz/adapters/trilogy.rb with RBS::Inline
2
+
3
+ module ActiveRecordConnectionTz
4
+ module Adapters
5
+ module Trilogy
6
+ # : () -> void
7
+ def self.install!: () -> void
8
+
9
+ module InstanceMethods
10
+ private
11
+
12
+ # : () -> void
13
+ def disable_temporal_cast: () -> void
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,53 @@
1
+ # Generated from lib/active_record_connection_tz/connection_settings.rb with RBS::Inline
2
+
3
+ module ActiveRecordConnectionTz
4
+ class ConnectionSettings
5
+ KEY: String
6
+
7
+ # Range used to detect whether a timezone stays fixed-offset for the practical
8
+ # lifetime of MySQL TIMESTAMP data, starting at the Unix epoch and extending
9
+ # slightly beyond 2038 to give the transition check some future slack.
10
+ AUTO_SESSION_TIME_ZONE_FROM: Time
11
+
12
+ AUTO_SESSION_TIME_ZONE_TO: Time
13
+
14
+ attr_reader time_zone_name: String?
15
+
16
+ attr_reader explicit_mysql_session_time_zone: String?
17
+
18
+ # : (Hash[untyped, untyped] config) -> ConnectionSettings
19
+ def self.from_config: (Hash[untyped, untyped] config) -> ConnectionSettings
20
+
21
+ # : (Hash[untyped, untyped]? raw) -> void
22
+ def initialize: (Hash[untyped, untyped]? raw) -> void
23
+
24
+ # : () -> bool
25
+ def enabled?: () -> bool
26
+
27
+ # : () -> ActiveSupport::TimeZone?
28
+ def time_zone: () -> ActiveSupport::TimeZone?
29
+
30
+ # : () -> Hash[String, String]?
31
+ def type_map_key: () -> Hash[String, String]?
32
+
33
+ # : () -> String?
34
+ def mysql_session_time_zone: () -> String?
35
+
36
+ # : (Hash[untyped, untyped] config) -> Hash[untyped, untyped]
37
+ def apply_mysql_variables: (Hash[untyped, untyped] config) -> Hash[untyped, untyped]
38
+
39
+ private
40
+
41
+ # : () -> String?
42
+ def auto_mysql_session_time_zone: () -> String?
43
+
44
+ # : () -> bool
45
+ def fixed_offset_time_zone?: () -> bool
46
+
47
+ # : (Integer offset_seconds) -> String
48
+ def format_utc_offset: (Integer offset_seconds) -> String
49
+
50
+ # : (untyped value, ?context: String) -> Hash[String, untyped]
51
+ def normalize_hash: (untyped value, ?context: String) -> Hash[String, untyped]
52
+ end
53
+ end
@@ -0,0 +1,11 @@
1
+ # Generated from lib/active_record_connection_tz/temporal_formatter.rb with RBS::Inline
2
+
3
+ module ActiveRecordConnectionTz
4
+ module TemporalFormatter
5
+ # : (Time | DateTime | ActiveSupport::TimeWithZone value, time_zone: ActiveSupport::TimeZone) -> String
6
+ def self.quoted_date: (Time | DateTime | ActiveSupport::TimeWithZone value, time_zone: ActiveSupport::TimeZone) -> String
7
+
8
+ # : (Time | DateTime | ActiveSupport::TimeWithZone value, time_zone: ActiveSupport::TimeZone) -> String
9
+ def self.quoted_time: (Time | DateTime | ActiveSupport::TimeWithZone value, time_zone: ActiveSupport::TimeZone) -> String
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ # Generated from lib/active_record_connection_tz/temporal_parser.rb with RBS::Inline
2
+
3
+ module ActiveRecordConnectionTz
4
+ module TemporalParser
5
+ # Treat MySQL year-zero and zero/partial-zero dates as nil instead of passing invalid dates to TimeZone parsing.
6
+ ZERO_DATETIME: Regexp
7
+
8
+ # : (String? value, time_zone: ActiveSupport::TimeZone) -> ActiveSupport::TimeWithZone?
9
+ def self.parse_datetime: (String? value, time_zone: ActiveSupport::TimeZone) -> ActiveSupport::TimeWithZone?
10
+
11
+ # : (String? value, time_zone: ActiveSupport::TimeZone) -> ActiveSupport::TimeWithZone?
12
+ def self.parse_time: (String? value, time_zone: ActiveSupport::TimeZone) -> ActiveSupport::TimeWithZone?
13
+ end
14
+ end
@@ -0,0 +1,20 @@
1
+ # Generated from lib/active_record_connection_tz/types/zoned_date_time.rb with RBS::Inline
2
+
3
+ module ActiveRecordConnectionTz
4
+ module Types
5
+ class ZonedDateTime < ActiveRecord::Type::DateTime
6
+ include ZonedTemporal
7
+
8
+ # : (untyped value) -> untyped
9
+ def cast: (untyped value) -> untyped
10
+
11
+ private
12
+
13
+ # : (String value) -> ActiveSupport::TimeWithZone?
14
+ def parse_string: (String value) -> ActiveSupport::TimeWithZone?
15
+
16
+ # : (untyped casted) -> String
17
+ def quote_casted: (untyped casted) -> String
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,33 @@
1
+ # Generated from lib/active_record_connection_tz/types/zoned_temporal.rb with RBS::Inline
2
+
3
+ module ActiveRecordConnectionTz
4
+ module Types
5
+ # @rbs module-self ActiveModel::Type::Value
6
+ module ZonedTemporal : ActiveModel::Type::Value
7
+ attr_reader time_zone: ActiveSupport::TimeZone
8
+
9
+ # : (time_zone: ActiveSupport::TimeZone) -> void
10
+ def initialize: (time_zone: ActiveSupport::TimeZone) -> void
11
+
12
+ # : (untyped value) -> untyped
13
+ def deserialize: (untyped value) -> untyped
14
+
15
+ # : (untyped value) -> untyped
16
+ def serialize: (untyped value) -> untyped
17
+
18
+ # : (untyped value) -> untyped
19
+ def serialize_cast_value: (untyped value) -> untyped
20
+
21
+ private
22
+
23
+ # : (untyped value) -> untyped
24
+ def serialize_temporal: (untyped value) -> untyped
25
+
26
+ # : (String _value) -> untyped
27
+ def parse_string: (String _value) -> untyped
28
+
29
+ # : (untyped _value) -> String
30
+ def quote_casted: (untyped _value) -> String
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,20 @@
1
+ # Generated from lib/active_record_connection_tz/types/zoned_time.rb with RBS::Inline
2
+
3
+ module ActiveRecordConnectionTz
4
+ module Types
5
+ class ZonedTime < ActiveRecord::Type::Time
6
+ include ZonedTemporal
7
+
8
+ # : (untyped value) -> untyped
9
+ def cast: (untyped value) -> untyped
10
+
11
+ private
12
+
13
+ # : (String value) -> ActiveSupport::TimeWithZone?
14
+ def parse_string: (String value) -> ActiveSupport::TimeWithZone?
15
+
16
+ # : (untyped casted) -> String
17
+ def quote_casted: (untyped casted) -> String
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,8 @@
1
+ # Generated from lib/active_record_connection_tz/types.rb with RBS::Inline
2
+
3
+ module ActiveRecordConnectionTz
4
+ module Types
5
+ # : (ActiveRecord::Type::TypeMap map, time_zone: ActiveSupport::TimeZone) -> void
6
+ def self.register_mysql_temporal_types!: (ActiveRecord::Type::TypeMap map, time_zone: ActiveSupport::TimeZone) -> void
7
+ end
8
+ end
@@ -0,0 +1,5 @@
1
+ # Generated from lib/active_record_connection_tz/version.rb with RBS::Inline
2
+
3
+ module ActiveRecordConnectionTz
4
+ VERSION: String
5
+ end
@@ -0,0 +1,17 @@
1
+ # Generated from lib/active_record_connection_tz.rb with RBS::Inline
2
+
3
+ module ActiveRecordConnectionTz
4
+ class Error < StandardError
5
+ end
6
+
7
+ class ConfigurationError < Error
8
+ end
9
+
10
+ INSTALL_MUTEX: Mutex
11
+
12
+ # : () -> void
13
+ def self.install!: () -> void
14
+
15
+ # : () -> bool
16
+ def self.installed?: () -> bool
17
+ end
@@ -0,0 +1,2 @@
1
+ # Generated from lib/activerecord-connection-tz.rb with RBS::Inline
2
+
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activerecord-connection-tz
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - shoma07
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activerecord
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.1'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '8.2'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '7.1'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '8.2'
32
+ description: |-
33
+ Provides connection-scoped timezone handling for applications that need to read and write
34
+ temporal columns with a configured timezone without per-attribute wiring.
35
+ email:
36
+ - 23730734+shoma07@users.noreply.github.com
37
+ executables: []
38
+ extensions: []
39
+ extra_rdoc_files: []
40
+ files:
41
+ - CHANGELOG.md
42
+ - LICENSE.txt
43
+ - README.md
44
+ - lib/active_record_connection_tz.rb
45
+ - lib/active_record_connection_tz/adapters/mysql2.rb
46
+ - lib/active_record_connection_tz/adapters/shared.rb
47
+ - lib/active_record_connection_tz/adapters/trilogy.rb
48
+ - lib/active_record_connection_tz/connection_settings.rb
49
+ - lib/active_record_connection_tz/temporal_formatter.rb
50
+ - lib/active_record_connection_tz/temporal_parser.rb
51
+ - lib/active_record_connection_tz/types.rb
52
+ - lib/active_record_connection_tz/types/zoned_date_time.rb
53
+ - lib/active_record_connection_tz/types/zoned_temporal.rb
54
+ - lib/active_record_connection_tz/types/zoned_time.rb
55
+ - lib/active_record_connection_tz/version.rb
56
+ - lib/activerecord-connection-tz.rb
57
+ - sig/active_record_connection_tz/adapters.rbs
58
+ - sig/generated/active_record_connection_tz.rbs
59
+ - sig/generated/active_record_connection_tz/adapters/mysql2.rbs
60
+ - sig/generated/active_record_connection_tz/adapters/shared.rbs
61
+ - sig/generated/active_record_connection_tz/adapters/trilogy.rbs
62
+ - sig/generated/active_record_connection_tz/connection_settings.rbs
63
+ - sig/generated/active_record_connection_tz/temporal_formatter.rbs
64
+ - sig/generated/active_record_connection_tz/temporal_parser.rbs
65
+ - sig/generated/active_record_connection_tz/types.rbs
66
+ - sig/generated/active_record_connection_tz/types/zoned_date_time.rbs
67
+ - sig/generated/active_record_connection_tz/types/zoned_temporal.rbs
68
+ - sig/generated/active_record_connection_tz/types/zoned_time.rbs
69
+ - sig/generated/active_record_connection_tz/version.rbs
70
+ - sig/generated/activerecord-connection-tz.rbs
71
+ homepage: https://github.com/shoma07/activerecord-connection-tz
72
+ licenses:
73
+ - MIT
74
+ metadata:
75
+ allowed_push_host: https://rubygems.org
76
+ homepage_uri: https://github.com/shoma07/activerecord-connection-tz
77
+ source_code_uri: https://github.com/shoma07/activerecord-connection-tz/tree/v0.1.0
78
+ changelog_uri: https://github.com/shoma07/activerecord-connection-tz/blob/v0.1.0/CHANGELOG.md
79
+ rubygems_mfa_required: 'true'
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: 3.2.0
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubygems_version: 4.0.3
95
+ specification_version: 4
96
+ summary: Connection-scoped timezone handling for mixed-timezone Active Record apps
97
+ test_files: []