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
|