require 'delegate' begin require "oci8" rescue LoadError => e # OCI8 driver is unavailable or failed to load a required library. raise LoadError, "ERROR: '#{e.message}'. "\ "ActiveRecord oracle_enhanced adapter could not load ruby-oci8 library. "\ "You may need install ruby-oci8 gem." end # check ruby-oci8 version required_oci8_version = [2, 2, 0] oci8_version_ints = OCI8::VERSION.scan(/\d+/).map{|s| s.to_i} if (oci8_version_ints <=> required_oci8_version) < 0 raise LoadError, "ERROR: ruby-oci8 version #{OCI8::VERSION} is too old. Please install ruby-oci8 version #{required_oci8_version.join('.')} or later." end module ActiveRecord module ConnectionAdapters # OCI database interface for MRI class OracleEnhancedOCIConnection < OracleEnhancedConnection #:nodoc: def initialize(config) @raw_connection = OCI8EnhancedAutoRecover.new(config, OracleEnhancedOCIFactory) # default schema owner @owner = config[:schema] @owner ||= config[:username] @owner = @owner.to_s.upcase end def raw_oci_connection if @raw_connection.is_a? OCI8 @raw_connection # ActiveRecord Oracle enhanced adapter puts OCI8EnhancedAutoRecover wrapper around OCI8 # in this case we need to pass original OCI8 connection else @raw_connection.instance_variable_get(:@connection) end end def auto_retry @raw_connection.auto_retry if @raw_connection end def auto_retry=(value) @raw_connection.auto_retry = value if @raw_connection end def logoff @raw_connection.logoff @raw_connection.active = false end def commit @raw_connection.commit end def rollback @raw_connection.rollback end def autocommit? @raw_connection.autocommit? end def autocommit=(value) @raw_connection.autocommit = value end # Checks connection, returns true if active. Note that ping actively # checks the connection, while #active? simply returns the last # known state. def ping @raw_connection.ping rescue OCIException => e raise OracleEnhancedConnectionException, e.message end def active? @raw_connection.active? end def reset! @raw_connection.reset! rescue OCIException => e raise OracleEnhancedConnectionException, e.message end def exec(sql, *bindvars, &block) @raw_connection.exec(sql, *bindvars, &block) end def returning_clause(quoted_pk) " RETURNING #{quoted_pk} INTO :insert_id" end # execute sql with RETURNING ... INTO :insert_id # and return :insert_id value def exec_with_returning(sql) cursor = @raw_connection.parse(sql) cursor.bind_param(':insert_id', nil, Integer) cursor.exec cursor[':insert_id'] ensure cursor.close rescue nil end def prepare(sql) Cursor.new(self, @raw_connection.parse(sql)) end class Cursor def initialize(connection, raw_cursor) @connection = connection @raw_cursor = raw_cursor end def bind_params( *bind_vars ) index = 1 bind_vars.flatten.each do |var| if Hash === var var.each { |key, val| bind_param key, val } else bind_param index, var index += 1 end end end def bind_param(position, value, column = nil) if column ActiveSupport::Deprecation.warn(<<-MSG.squish) ******************************************************* Passing a column to `bind_param` will be deprecated. `type_casted_binds` should be already type casted so that `bind_param` should not need to know column. ******************************************************* MSG end if column && column.object_type? @raw_cursor.bind_param(position, value, :named_type, column.sql_type) else case value when ActiveRecord::OracleEnhanced::Type::Raw @raw_cursor.bind_param(position, ActiveRecord::ConnectionAdapters::OracleEnhanced::Quoting.encode_raw(value)) when ActiveModel::Type::Decimal @raw_cursor.bind_param(position, BigDecimal.new(value.to_s)) when NilClass @raw_cursor.bind_param(position, nil, String) else @raw_cursor.bind_param(position, value) end end end def bind_returning_param(position, bind_type) @raw_cursor.bind_param(position, nil, bind_type) end def exec @raw_cursor.exec end def exec_update @raw_cursor.exec end def get_col_names @raw_cursor.get_col_names end def fetch(options={}) if row = @raw_cursor.fetch get_lob_value = options[:get_lob_value] row.map do |col| @connection.typecast_result_value(col, get_lob_value) end end end def get_returning_param(position, type) @raw_cursor[position] end def close @raw_cursor.close end end def select(sql, name = nil, return_column_names = false) cursor = @raw_connection.exec(sql) cols = [] # Ignore raw_rnum_ which is used to simulate LIMIT and OFFSET cursor.get_col_names.each do |col_name| col_name = oracle_downcase(col_name) cols << col_name unless col_name == 'raw_rnum_' end # Reuse the same hash for all rows column_hash = {} cols.each {|c| column_hash[c] = nil} rows = [] get_lob_value = !(name == 'Writable Large Object') while row = cursor.fetch hash = column_hash.dup cols.each_with_index do |col, i| hash[col] = typecast_result_value(row[i], get_lob_value) end rows << hash end return_column_names ? [rows, cols] : rows ensure cursor.close if cursor end def write_lob(lob, value, is_binary = false) lob.write value end def describe(name) # fall back to SELECT based describe if using database link return super if name.to_s.include?('@') quoted_name = ActiveRecord::ConnectionAdapters::OracleEnhanced::Quoting.valid_table_name?(name) ? name : "\"#{name}\"" @raw_connection.describe(quoted_name) rescue OCIException => e if e.code == 4043 raise OracleEnhancedConnectionException, %Q{"DESC #{name}" failed; does it exist?} else # fall back to SELECT which can handle synonyms to database links super end end # Return OCIError error code def error_code(exception) case exception when OCIError exception.code else nil end end def typecast_result_value(value, get_lob_value) case value when Fixnum, Bignum value when String value when Float, BigDecimal # return Fixnum or Bignum if value is integer (to avoid issues with _before_type_cast values for id attributes) value == (v_to_i = value.to_i) ? v_to_i : value when OraNumber # change OraNumber value (returned in early versions of ruby-oci8 2.0.x) to BigDecimal value == (v_to_i = value.to_i) ? v_to_i : BigDecimal.new(value.to_s) when OCI8::LOB if get_lob_value data = value.read || "" # if value.read returns nil, then we have an empty_clob() i.e. an empty string # In Ruby 1.9.1 always change encoding to ASCII-8BIT for binaries data.force_encoding('ASCII-8BIT') if data.respond_to?(:force_encoding) && value.is_a?(OCI8::BLOB) data else value end when Time, DateTime if OracleEnhancedAdapter.emulate_dates && date_without_time?(value) value.to_date else create_time_with_default_timezone(value) end else value end end def database_version @database_version ||= (version = raw_connection.oracle_server_version) && [version.major, version.minor] end private def date_without_time?(value) case value when OraDate value.hour == 0 && value.minute == 0 && value.second == 0 else value.hour == 0 && value.min == 0 && value.sec == 0 end end def create_time_with_default_timezone(value) year, month, day, hour, min, sec, usec = case value when Time [value.year, value.month, value.day, value.hour, value.min, value.sec, value.usec] when OraDate [value.year, value.month, value.day, value.hour, value.minute, value.second, 0] else [value.year, value.month, value.day, value.hour, value.min, value.sec, 0] end # code from Time.time_with_datetime_fallback begin Time.send(Base.default_timezone, year, month, day, hour, min, sec, usec) rescue offset = Base.default_timezone.to_sym == :local ? ::DateTime.local_offset : 0 ::DateTime.civil(year, month, day, hour, min, sec, offset) end end end # The OracleEnhancedOCIFactory factors out the code necessary to connect and # configure an Oracle/OCI connection. class OracleEnhancedOCIFactory #:nodoc: def self.new_connection(config) # to_s needed if username, password or database is specified as number in database.yml file username = config[:username] && config[:username].to_s password = config[:password] && config[:password].to_s database = config[:database] && config[:database].to_s schema = config[:schema] && config[:schema].to_s host, port = config[:host], config[:port] privilege = config[:privilege] && config[:privilege].to_sym async = config[:allow_concurrency] prefetch_rows = config[:prefetch_rows] || 100 cursor_sharing = config[:cursor_sharing] || 'force' # get session time_zone from configuration or from TZ environment variable time_zone = config[:time_zone] || ENV['TZ'] # connection using host, port and database name connection_string = if host || port host ||= 'localhost' host = "[#{host}]" if host =~ /^[^\[].*:/ # IPv6 port ||= 1521 database = "/#{database}" unless database.match(/^\//) "//#{host}:#{port}#{database}" # if no host is specified then assume that # database parameter is TNS alias or TNS connection string else database end conn = OCI8.new username, password, connection_string, privilege conn.autocommit = true conn.non_blocking = true if async conn.prefetch_rows = prefetch_rows conn.exec "alter session set cursor_sharing = #{cursor_sharing}" rescue nil if ActiveRecord::Base.default_timezone == :local conn.exec "alter session set time_zone = '#{time_zone}'" unless time_zone.blank? elsif ActiveRecord::Base.default_timezone == :utc conn.exec "alter session set time_zone = '+00:00'" end conn.exec "alter session set current_schema = #{schema}" unless schema.blank? # Initialize NLS parameters OracleEnhancedAdapter::DEFAULT_NLS_PARAMETERS.each do |key, default_value| value = config[key] || ENV[key.to_s.upcase] || default_value if value conn.exec "alter session set #{key} = '#{value}'" end end conn end end end end class OCI8 #:nodoc: def describe(name) info = describe_table(name.to_s) raise %Q{"DESC #{name}" failed} if info.nil? if info.respond_to? :obj_link and info.obj_link [info.obj_schema, info.obj_name, '@' + info.obj_link] else [info.obj_schema, info.obj_name] end end end # The OCI8AutoRecover class enhances the OCI8 driver with auto-recover and # reset functionality. If a call to #exec fails, and autocommit is turned on # (ie., we're not in the middle of a longer transaction), it will # automatically reconnect and try again. If autocommit is turned off, # this would be dangerous (as the earlier part of the implied transaction # may have failed silently if the connection died) -- so instead the # connection is marked as dead, to be reconnected on it's next use. #:stopdoc: class OCI8EnhancedAutoRecover < DelegateClass(OCI8) #:nodoc: attr_accessor :active #:nodoc: alias :active? :active #:nodoc: cattr_accessor :auto_retry class << self alias :auto_retry? :auto_retry #:nodoc: end @@auto_retry = false def initialize(config, factory) #:nodoc: @active = true @config = config @factory = factory @connection = @factory.new_connection @config super @connection end # Checks connection, returns true if active. Note that ping actively # checks the connection, while #active? simply returns the last # known state. def ping #:nodoc: @connection.exec("select 1 from dual") { |r| nil } @active = true rescue @active = false raise end # Resets connection, by logging off and creating a new connection. def reset! #:nodoc: logoff rescue nil begin @connection = @factory.new_connection @config __setobj__ @connection @active = true rescue @active = false raise end end # ORA-00028: your session has been killed # ORA-01012: not logged on # ORA-03113: end-of-file on communication channel # ORA-03114: not connected to ORACLE # ORA-03135: connection lost contact LOST_CONNECTION_ERROR_CODES = [ 28, 1012, 3113, 3114, 3135 ] #:nodoc: # Adds auto-recovery functionality. # # See: http://www.jiubao.org/ruby-oci8/api.en.html#label-11 def exec(sql, *bindvars, &block) #:nodoc: should_retry = self.class.auto_retry? && autocommit? begin @connection.exec(sql, *bindvars, &block) rescue OCIException => e raise unless e.is_a?(OCIError) && LOST_CONNECTION_ERROR_CODES.include?(e.code) @active = false raise unless should_retry should_retry = false reset! rescue nil retry end end end #:startdoc: