require 'date' class OCI8 module BindType # Returns the default time zone when using Oracle 8.x client. # The value is unused when using Oracle 9i or upper client. # # See also: OCI8::BindType::Time # # @return [:local or :utc] def self.default_timezone OCI8::BindType::Util.default_timezone end # Sets the default time zone when using Oracle 8.x client. # The value is unused when using Oracle 9i or upper client. # # See also: OCI8::BindType::Time # # @param [:local or :utc] tz def self.default_timezone=(tz) OCI8::BindType::Util.default_timezone = tz end module Util # :nodoc: @@datetime_fsec_base = (1 / ::DateTime.parse('0001-01-01 00:00:00.000000001').sec_fraction).to_i @@default_timezone = :local begin Time.new(2001, 1, 1, 0, 0, 0, '+00:00') @@time_new_accepts_timezone = true # after ruby 1.9.2 rescue ArgumentError @@time_new_accepts_timezone = false # prior to ruby 1.9.2 end begin # 2001-01-01 00:00:59.999 ::DateTime.civil(2001, 1, 1, 0, 0, Rational(59_999, 1000), 0) @@datetime_has_fractional_second_bug = false rescue ArgumentError @@datetime_has_fractional_second_bug = true end def self.default_timezone @@default_timezone end def self.default_timezone=(tz) if tz != :local and tz != :utc raise ArgumentError, "expected :local or :utc but #{tz}" end @@default_timezone = tz end private def datetime_to_array(val, datatype) return nil if val.nil? # year year = val.year # month if val.respond_to? :mon month = val.mon elsif val.respond_to? :month month = val.month else raise "expect Time, Date or DateTime but #{val.class}" end # day if val.respond_to? :mday day = val.mday elsif val.respond_to? :day day = val.day else raise "expect Time, Date or DateTime but #{val.class}" end # hour if val.respond_to? :hour hour = val.hour else hour = 0 end # minute if val.respond_to? :min minute = val.min else minute = 0 end # second if val.respond_to? :sec sec = val.sec else sec = 0 end return [year, month, day, hour, minute, sec] if datatype == :date # fractional second if val.respond_to? :sec_fraction fsec = (val.sec_fraction * @@datetime_fsec_base).to_i elsif val.respond_to? :nsec fsec = val.nsec elsif val.respond_to? :usec fsec = val.usec * 1000 else fsec = 0 end return [year, month, day, hour, minute, sec, fsec, nil, nil] if datatype == :timestamp # time zone if val.respond_to? :offset # DateTime tz_min = (val.offset * 1440).to_i elsif val.respond_to? :utc_offset # Time tz_min = val.utc_offset / 60 else tz_hour = nil tz_min = nil end if tz_min if tz_min < 0 tz_min = - tz_min tz_hour = - (tz_min / 60) tz_min = (tz_min % 60) else tz_hour = tz_min / 60 tz_min = tz_min % 60 end end [year, month, day, hour, minute, sec, fsec, tz_hour, tz_min] end def array_to_datetime(ary, timezone) return nil if ary.nil? year, month, day, hour, minute, sec, nsec, tz_hour, tz_min = ary sec += nsec.to_r / 1000000000 if nsec and nsec != 0 if tz_hour and tz_min offset = tz_hour.to_r / 24 + tz_min.to_r / 1440 else if @@default_timezone == :local if ::DateTime.respond_to? :local_offset offset = ::DateTime.local_offset # Use a method defined by active support. else # Do as active support does. offset = ::Time.local(2007).utc_offset.to_r / 86400 end else offset = 0 end end if @@datetime_has_fractional_second_bug and sec >= 59 and nsec != 0 # convert to a DateTime via a String as a workaround if offset >= 0 sign = ?+ else sign = ?- offset = - offset; end tz_min = (offset * 1440).to_i tz_hour, tz_min = tz_min.divmod 60 time_str = format("%04d-%02d-%02dT%02d:%02d:%02d.%09d%c%02d:%02d", year, month, day, hour, minute, sec, nsec, sign, tz_hour, tz_min) ::DateTime.parse(time_str) else ::DateTime.civil(year, month, day, hour, minute, sec, offset) end end if @@time_new_accepts_timezone # after ruby 1.9.2 def array_to_time(ary, timezone) return nil if ary.nil? year, month, day, hour, minute, sec, nsec, tz_hour, tz_min = ary nsec ||= 0 if timezone usec = (nsec == 0) ? 0 : nsec.to_r / 1000 ::Time.send(timezone, year, month, day, hour, minute, sec, usec) else sec += nsec.to_r / 1_000_000_000 if nsec != 0 utc_offset = tz_hour * 3600 + tz_min * 60 ::Time.new(year, month, day, hour, minute, sec, utc_offset) end end else # prior to ruby 1.9.2 def array_to_time(ary, timezone) return nil if ary.nil? year, month, day, hour, minute, sec, nsec, tz_hour, tz_min = ary nsec ||= 0 usec = (nsec == 0) ? 0 : nsec.to_r / 1000 begin if timezone return ::Time.send(timezone, year, month, day, hour, minute, sec, usec) else if tz_hour == 0 and tz_min == 0 tm = ::Time.utc(year, month, day, hour, minute, sec, usec) # Time.utc(99, ...) returns a time object the year of which is 1999. # 'tm.year == year' checks such cases. return tm if tm.year == year else tm = ::Time.local(year, month, day, hour, minute, sec, usec) return tm if tm.utc_offset == tz_hour * 3600 + tz_min * 60 and tm.year == year end end rescue StandardError end array_to_datetime(ary, timezone) end end end #-- # OCI8::BindType::DateTime #++ # This is a helper class to select or bind Oracle data types such as # DATE, TIMESTAMP, TIMESTAMP WITH TIME ZONE # and TIMESTAMP WITH LOCAL TIME ZONE. The retrieved value # is a \DateTime. # # === How to select \DataTime values. # # DATE, TIMESTAMP, TIMESTAMP WITH TIME ZONE # and TIMESTAMP WITH LOCAL TIME ZONE are selected as a \Time # by default. You change the behaviour by explicitly calling # OCI8::Cursor#define as follows: # # cursor = conn.parse("SELECT hiredate FROM emp") # cursor.define(1, nil, DateTime) # cursor.exec() # # Otherwise, you can change the default mapping for all queries. # # # Changes the mapping for DATE # OCI8::BindType::Mapping[OCI8::SQLT_DAT] = OCI8::BindType::DateTime # # # Changes the mapping for TIMESTAMP # OCI8::BindType::Mapping[OCI8::SQLT_TIMESTAMP] = OCI8::BindType::DateTime # # # Changes the mapping for TIMESTAMP WITH TIME ZONE # OCI8::BindType::Mapping[OCI8::SQLT_TIMESTAMP_TZ] = OCI8::BindType::DateTime # # # Changes the mapping for TIMESTAMP WITH LOCAL TIME ZONE # OCI8::BindType::Mapping[OCI8::SQLT_TIMESTAMP_LTZ] = OCI8::BindType::DateTime # # === Note for default time zone # # The retrieved value's time zone is determined by the session time zone # if its data type is DATE, TIMESTAMP or TIMESTAMP # WITH LOCAL TIME ZONE. # # The session time zone is same with local machine's by default. # It is changed by the following SQL. # # ALTER SESSION SET TIME_ZONE='-05:00' # # === Note for Oracle 8.x client # # Timestamp data types and session time zone are new features in # Oracle 9i. This class is available only to fetch or bind DATE # when using Oracle 8.x client. # # The retrieved value's time zone is determined not by the session # time zone, but by the OCI8::BindType.default_timezone # The time zone can be changed as follows: # # OCI8::BindType.default_timezone = :local # # or # OCI8::BindType.default_timezone = :utc # # If you are in the regions where daylight saving time is adopted, # you should use OCI8::BindType::Time. # class DateTime < OCI8::BindType::OCITimestampTZ include OCI8::BindType::Util def set(val) # :nodoc: super(datetime_to_array(val, :timestamp_tz)) end def get() # :nodoc: array_to_datetime(super(), nil) end end class LocalDateTime < OCI8::BindType::OCITimestamp include OCI8::BindType::Util def set(val) # :nodoc: super(datetime_to_array(val, :timestamp)) end def get() # :nodoc: array_to_datetime(super(), :local) end end class UTCDateTime < OCI8::BindType::OCITimestamp include OCI8::BindType::Util def set(val) # :nodoc: super(datetime_to_array(val, :timestamp)) end def get() # :nodoc: array_to_datetime(super(), :utc) end end #-- # OCI8::BindType::Time #++ # This is a helper class to select or bind Oracle data types such as # DATE, TIMESTAMP, TIMESTAMP WITH TIME ZONE # and TIMESTAMP WITH LOCAL TIME ZONE. The retrieved value # is a \Time. # # === How to select \Time values. # # DATE, TIMESTAMP, TIMESTAMP WITH TIME ZONE # and TIMESTAMP WITH LOCAL TIME ZONE are selected as a \Time # by default. If the default behaviour is changed, you can select it # as a \Time by explicitly calling OCI8::Cursor#define as follows: # # cursor = conn.parse("SELECT hiredate FROM emp") # cursor.define(1, nil, Time) # cursor.exec() # # === Note for ruby prior to 1.9.2 # # If the retrieved value cannot be represented by \Time, it become # a \DateTime. The fallback is done only when the ruby is before 1.9.2 # and one of the following conditions are met. # - The timezone part is neither local nor utc. # - The time is out of the time_t[http://en.wikipedia.org/wiki/Time_t]. # # If the retrieved value has the precision of fractional second more # than 6, the fractional second is truncated to microsecond, which # is the precision of standard \Time class. # # To avoid this fractional second truncation: # - Upgrade to ruby 1.9.2, whose \Time precision is nanosecond. # - Otherwise, change the defalt mapping to use \DateTime as follows. # OCI8::BindType::Mapping[OCI8::SQLT_TIMESTAMP] = OCI8::BindType::DateTime # OCI8::BindType::Mapping[OCI8::SQLT_TIMESTAMP_TZ] = OCI8::BindType::DateTime # OCI8::BindType::Mapping[OCI8::SQLT_TIMESTAMP_LTZ] = OCI8::BindType::DateTime # # === Note for default time zone # # The retrieved value's time zone is determined by the session time zone # if its data type is DATE, TIMESTAMP or TIMESTAMP # WITH LOCAL TIME ZONE. # # The session time zone is same with local machine's by default. # It is changed by the following SQL. # # ALTER SESSION SET TIME_ZONE='-05:00' # # === Note for Oracle 8.x client # # Timestamp data types and session time zone are new features in # Oracle 9i. This class is available only to fetch or bind DATE # when using Oracle 8.x client. # # The retrieved value's time zone is determined not by the session # time zone, but by the OCI8::BindType.default_timezone # The time zone can be changed as follows: # # OCI8::BindType.default_timezone = :local # # or # OCI8::BindType.default_timezone = :utc # class Time < OCI8::BindType::OCITimestampTZ include OCI8::BindType::Util def set(val) # :nodoc: super(datetime_to_array(val, :timestamp_tz)) end def get() # :nodoc: array_to_time(super(), nil) end end class LocalTime < OCI8::BindType::OCITimestamp include OCI8::BindType::Util def set(val) # :nodoc: super(datetime_to_array(val, :timestamp)) end def get() # :nodoc: array_to_time(super(), :local) end end class UTCTime < OCI8::BindType::OCITimestamp include OCI8::BindType::Util def set(val) # :nodoc: super(datetime_to_array(val, :timestamp)) end def get() # :nodoc: array_to_time(super(), :utc) end end #-- # OCI8::BindType::IntervalYM #++ # # This is a helper class to select or bind Oracle data type # INTERVAL YEAR TO MONTH. The retrieved value is # the number of months between two timestamps. # # The value can be applied to \DateTime#>> to shift months. # It can be applied to \Time#months_since if activisupport has # been loaded. # # === How to select INTERVAL YEAR TO MONTH # # INTERVAL YEAR TO MONTH is selected as an Integer. # # conn.exec("select (current_timestamp - hiredate) year to month from emp") do |hired_months| # puts "hired_months = #{hired_months}" # end # # == How to bind INTERVAL YEAR TO MONTH # # You cannot bind a bind variable as INTERVAL YEAR TO MONTH implicitly. # It must be bound explicitly by OCI8::Cursor#bind_param. # # # output bind variable # cursor = conn.parse(<<-EOS) # BEGIN # :interval := (:ts1 - :ts2) YEAR TO MONTH; # END; # EOS # cursor.bind_param(:interval, nil, :interval_ym) # cursor.bind_param(:ts1, DateTime.parse('1969-11-19 06:54:35 00:00')) # cursor.bind_param(:ts2, DateTime.parse('1969-07-20 20:17:40 00:00')) # cursor.exec # cursor[:interval] # => 4 (months) # cursor.close # # # input bind variable # cursor = conn.parse(<<-EOS) # BEGIN # :ts1 := :ts2 + :interval; # END; # EOS # cursor.bind_param(:ts1, nil, DateTime) # cursor.bind_param(:ts2, Date.parse('1969-11-19')) # cursor.bind_param(:interval, 4, :interval_ym) # cursor.exec # cursor[:ts1].strftime('%Y-%m-%d') # => 1970-03-19 # cursor.close # class IntervalYM < OCI8::BindType::OCIIntervalYM def set(val) # :nodoc: unless val.nil? val = [val / 12, val % 12] end super(val) end def get() # :nodoc: val = super() return nil if val.nil? year, month = val year * 12 + month end end # OCI8::BindType::IntervalYM #-- # OCI8::BindType::IntervalDS #++ # # (new in 2.0) # # This is a helper class to select or bind Oracle data type # INTERVAL DAY TO SECOND. The retrieved value is # the number of seconds between two typestamps as a \Float. # # Note that it is the number days as a \Rational if # OCI8::BindType::IntervalDS.unit is :day or the ruby-oci8 # version is prior to 2.0.3. # # == How to bind INTERVAL DAY TO SECOND # # You cannot bind a bind variable as INTERVAL DAY TO SECOND # implicitly. It must be bound explicitly by OCI8::Cursor#bind_param. # # # output bind variable # cursor = conn.parse(<<-EOS) # BEGIN # :interval := (:ts1 - :ts2) DAY TO SECOND(9); # END; # EOS # cursor.bind_param(:interval, nil, :interval_ds) # cursor.bind_param(:ts1, DateTime.parse('1969-11-19 06:54:35 00:00')) # cursor.bind_param(:ts2, DateTime.parse('1969-07-20 20:17:40 00:00')) # cursor.exec # cursor[:interval] # => 10492615.0 seconds # cursor.close # # # input bind variable # cursor = conn.parse(<<-EOS) # BEGIN # :ts1 := :ts2 + :interval; # END; # EOS # cursor.bind_param(:ts1, nil, DateTime) # cursor.bind_param(:ts2, DateTime.parse('1969-07-20 20:17:40 00:00')) # cursor.bind_param(:interval, 10492615.0, :interval_ds) # cursor.exec # cursor[:ts1].strftime('%Y-%m-%d %H:%M:%S') # => 1969-11-19 06:54:35 # cursor.close # class IntervalDS < OCI8::BindType::OCIIntervalDS @@hour = 1 / 24.to_r @@minute = @@hour / 60 @@sec = @@minute / 60 @@fsec = @@sec / 1000000000 @@unit = :second # Retrieves the unit of interval. # # @return [:second or :day] # @since 2.0.3 def self.unit @@unit end # Changes the unit of interval. :second is the default. # # @param [:second or :day] val # @since 2.0.3 def self.unit=(val) case val when :second, :day @@unit = val else raise 'unit should be :second or :day' end end def set(val) # :nodoc: unless val.nil? if val < 0 is_minus = true val = -val else is_minus = false end if @@unit == :second day, val = val.divmod 86400 hour, val = val.divmod 3600 minute, val = val.divmod 60 sec, val = val.divmod 1 else day, val = val.divmod 1 hour, val = (val * 24).divmod 1 minute, val = (val * 60).divmod 1 sec, val = (val * 60).divmod 1 end fsec, val = (val * 1000000000).divmod 1 if is_minus day = - day hour = - hour minute = - minute sec = - sec fsec = - fsec end val = [day, hour, minute, sec, fsec] end super(val) end def get() # :nodoc: val = super() return nil if val.nil? day, hour, minute, sec, fsec = val if @@unit == :second fsec = fsec / 1000000000.0 day * 86400 + hour * 3600 + minute * 60 + sec + fsec else day + (hour * @@hour) + (minute * @@minute) + (sec * @@sec) + (fsec * @@fsec) end end end # OCI8::BindType::IntervalDS end # OCI8::BindType end # OCI8