File: pg_interval.rb

package info (click to toggle)
ruby-sequel 5.63.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 10,408 kB
  • sloc: ruby: 113,747; makefile: 3
file content (224 lines) | stat: -rw-r--r-- 7,569 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# frozen-string-literal: true
#
# The pg_interval extension adds support for PostgreSQL's interval type.
#
# This extension integrates with Sequel's native postgres and jdbc/postgresql
# adapters, so that when interval type values are retrieved, they are parsed and returned
# as instances of ActiveSupport::Duration.
#
# In addition to the parser, this extension adds literalizers for
# ActiveSupport::Duration that use the standard Sequel literalization
# callbacks, so they work on all adapters.
#
# To use this extension, load it into the Database instance:
#
#   DB.extension :pg_interval
#
# This extension integrates with the pg_array extension.  If you plan
# to use arrays of interval types, load the pg_array extension before the
# pg_interval extension:
#
#   DB.extension :pg_array, :pg_interval
#
# The parser this extension uses requires that IntervalStyle for PostgreSQL
# is set to postgres (the default setting).  If IntervalStyle is changed from
# the default setting, the parser will probably not work.  The parser used is
# very simple, and is only designed to parse PostgreSQL's default output
# format, it is not designed to support all input formats that PostgreSQL
# supports.
#
# See the {schema modification guide}[rdoc-ref:doc/schema_modification.rdoc]
# for details on using interval columns in CREATE/ALTER TABLE statements.
#
# Related module: Sequel::Postgres::IntervalDatabaseMethods

require 'active_support'
require 'active_support/duration'

# :nocov:
begin
  require 'active_support/version'
rescue LoadError
end
# :nocov:

module Sequel
  module Postgres
    module IntervalDatabaseMethods
      DURATION_UNITS = [:years, :months, :weeks, :days, :hours, :minutes, :seconds].freeze

      # Return an unquoted string version of the duration object suitable for
      # use as a bound variable.
      def self.literal_duration(duration)
        h = Hash.new(0)
        duration.parts.each{|unit, value| h[unit] += value}
        s = String.new

        DURATION_UNITS.each do |unit|
          if (v = h[unit]) != 0
            s << "#{v.is_a?(Integer) ? v : sprintf('%0.6f', v)} #{unit} "
          end
        end

        if s.empty?
          '0'
        else
          s
        end
      end

      # Creates callable objects that convert strings into ActiveSupport::Duration instances.
      class Parser
        # Whether ActiveSupport::Duration.new takes parts as array instead of hash
        USE_PARTS_ARRAY = !defined?(ActiveSupport::VERSION::STRING) || ActiveSupport::VERSION::STRING < '5.1'

        if defined?(ActiveSupport::Duration::SECONDS_PER_MONTH)
          SECONDS_PER_MONTH = ActiveSupport::Duration::SECONDS_PER_MONTH
          SECONDS_PER_YEAR = ActiveSupport::Duration::SECONDS_PER_YEAR
        # :nocov:
        else
          SECONDS_PER_MONTH = 2592000
          SECONDS_PER_YEAR = 31557600
        # :nocov:
        end

        # Parse the interval input string into an ActiveSupport::Duration instance.
        def call(string)
          raise(InvalidValue, "invalid or unhandled interval format: #{string.inspect}") unless matches = /\A([+-]?\d+ years?\s?)?([+-]?\d+ mons?\s?)?([+-]?\d+ days?\s?)?(?:(?:([+-])?(\d{2,10}):(\d\d):(\d\d(\.\d+)?))|([+-]?\d+ hours?\s?)?([+-]?\d+ mins?\s?)?([+-]?\d+(\.\d+)? secs?\s?)?)?\z/.match(string)

          value = 0
          parts = {}

          if v = matches[1]
            v = v.to_i
            value += SECONDS_PER_YEAR * v
            parts[:years] = v
          end
          if v = matches[2]
            v = v.to_i
            value += SECONDS_PER_MONTH * v
            parts[:months] = v
          end
          if v = matches[3]
            v = v.to_i
            value += 86400 * v
            parts[:days] = v
          end
          if matches[5]
            seconds = matches[5].to_i * 3600 + matches[6].to_i * 60
            seconds += matches[8] ? matches[7].to_f : matches[7].to_i
            seconds *= -1 if matches[4] == '-'
            value += seconds
            parts[:seconds] = seconds
          elsif matches[9] || matches[10] || matches[11]
            seconds = 0
            if v = matches[9]
              seconds += v.to_i * 3600
            end
            if v = matches[10]
              seconds += v.to_i * 60
            end
            if v = matches[11]
              seconds += matches[12] ? v.to_f : v.to_i
            end
            value += seconds
            parts[:seconds] = seconds
          end

          # :nocov:
          if USE_PARTS_ARRAY
            parts = parts.to_a
          end
          # :nocov:

          ActiveSupport::Duration.new(value, parts)
        end
      end

      # Single instance of Parser used for parsing, to save on memory (since the parser has no state).
      PARSER = Parser.new

      # Reset the conversion procs if using the native postgres adapter,
      # and extend the datasets to correctly literalize ActiveSupport::Duration values.
      def self.extended(db)
        db.instance_exec do
          extend_datasets(IntervalDatasetMethods)
          add_conversion_proc(1186, Postgres::IntervalDatabaseMethods::PARSER)
          if respond_to?(:register_array_type)
            register_array_type('interval', :oid=>1187, :scalar_oid=>1186)
          end
          @schema_type_classes[:interval] = ActiveSupport::Duration
        end
      end

      # Handle ActiveSupport::Duration values in bound variables.
      def bound_variable_arg(arg, conn)
        case arg
        when ActiveSupport::Duration
          IntervalDatabaseMethods.literal_duration(arg)
        else
          super
        end
      end

      private

      # Set the :ruby_default value if the default value is recognized as an interval.
      def schema_post_process(_)
        super.each do |a|
          h = a[1]
          if h[:type] == :interval && h[:default] =~ /\A'([\w ]+)'::interval\z/
            h[:ruby_default] = PARSER.call($1)
          end
        end
      end

      # Typecast value correctly to an ActiveSupport::Duration instance.
      # If already an ActiveSupport::Duration, return it. 
      # If a numeric argument is given, assume it represents a number
      # of seconds, and create a new ActiveSupport::Duration instance
      # representing that number of seconds.
      # If a String, assume it is in PostgreSQL interval output format
      # and attempt to parse it.
      def typecast_value_interval(value)
        case value
        when ActiveSupport::Duration
          value
        when Numeric
          ActiveSupport::Duration.new(value, [[:seconds, value]])
        when String
          PARSER.call(typecast_check_string_length(value, 1000))
        else
          raise Sequel::InvalidValue, "invalid value for interval type: #{value.inspect}"
        end
      end
    end

    module IntervalDatasetMethods
      private

      # Allow auto parameterization of ActiveSupport::Duration instances.
      def auto_param_type_fallback(v)
        if defined?(super) && (type = super)
          type
        elsif ActiveSupport::Duration === v
          "::interval"
        end
      end

      # Handle literalization of ActiveSupport::Duration objects, treating them as
      # PostgreSQL intervals.
      def literal_other_append(sql, v)
        case v
        when ActiveSupport::Duration
          literal_append(sql, IntervalDatabaseMethods.literal_duration(v))
          sql << '::interval'
        else
          super
        end
      end
    end
  end

  Database.register_extension(:pg_interval, Postgres::IntervalDatabaseMethods)
end