# -*- coding: utf-8 -*- # oracle_enhanced_adapter.rb -- ActiveRecord adapter for Oracle 8i, 9i, 10g, 11g # # Authors or original oracle_adapter: Graham Jenkins, Michael Schoen # # Current maintainer: Raimonds Simanovskis (http://blog.rayapps.com) # ######################################################################### # # See History.md for changes added to original oracle_adapter.rb # ######################################################################### # # From original oracle_adapter.rb: # # Implementation notes: # 1. Redefines (safely) a method in ActiveRecord to make it possible to # implement an autonumbering solution for Oracle. # 2. The OCI8 driver is patched to properly handle values for LONG and # TIMESTAMP columns. The driver-author has indicated that a future # release of the driver will obviate this patch. # 3. LOB support is implemented through an after_save callback. # 4. Oracle does not offer native LIMIT and OFFSET options; this # functionality is mimiced through the use of nested selects. # See http://asktom.oracle.com/pls/ask/f?p=4950:8:::::F4950_P8_DISPLAYID:127412348064 # # Do what you want with this code, at your own peril, but if any # significant portion of my code remains then please acknowledge my # contribution. # portions Copyright 2005 Graham Jenkins # ActiveRecord 2.2 does not load version file automatically require 'active_record/version' unless defined?(ActiveRecord::VERSION) require 'active_record/connection_adapters/abstract_adapter' require 'active_record/connection_adapters/oracle_enhanced_connection' require 'active_record/connection_adapters/oracle_enhanced_base_ext' require 'active_record/connection_adapters/oracle_enhanced_column' require 'digest/sha1' module ActiveRecord module ConnectionAdapters #:nodoc: # Oracle enhanced adapter will work with both # Ruby 1.8/1.9 ruby-oci8 gem (which provides interface to Oracle OCI client) # or with JRuby and Oracle JDBC driver. # # It should work with Oracle 9i, 10g and 11g databases. # Limited set of functionality should work on Oracle 8i as well but several features # rely on newer functionality in Oracle database. # # Usage notes: # * Key generation assumes a "${table_name}_seq" sequence is available # for all tables; the sequence name can be changed using # ActiveRecord::Base.set_sequence_name. When using Migrations, these # sequences are created automatically. # Use set_sequence_name :autogenerated with legacy tables that have # triggers that populate primary keys automatically. # * Oracle uses DATE or TIMESTAMP datatypes for both dates and times. # Consequently some hacks are employed to map data back to Date or Time # in Ruby. Timezones and sub-second precision on timestamps are # not supported. # * Default values that are functions (such as "SYSDATE") are not # supported. This is a restriction of the way ActiveRecord supports # default values. # # Required parameters: # # * :username # * :password # * :database - either TNS alias or connection string for OCI client or database name in JDBC connection string # # Optional parameters: # # * :host - host name for JDBC connection, defaults to "localhost" # * :port - port number for JDBC connection, defaults to 1521 # * :privilege - set "SYSDBA" if you want to connect with this privilege # * :allow_concurrency - set to "true" if non-blocking mode should be enabled (just for OCI client) # * :prefetch_rows - how many rows should be fetched at one time to increase performance, defaults to 100 # * :cursor_sharing - cursor sharing mode to minimize amount of unique statements, defaults to "force" # * :time_zone - database session time zone # (it is recommended to set it using ENV['TZ'] which will be then also used for database session time zone) # # Optionals NLS parameters: # # * :nls_calendar # * :nls_comp # * :nls_currency # * :nls_date_format - format for :date columns, defaults to YYYY-MM-DD HH24:MI:SS # * :nls_date_language # * :nls_dual_currency # * :nls_iso_currency # * :nls_language # * :nls_length_semantics - semantics of size of VARCHAR2 and CHAR columns, defaults to CHAR # (meaning that size specifies number of characters and not bytes) # * :nls_nchar_conv_excp # * :nls_numeric_characters # * :nls_sort # * :nls_territory # * :nls_timestamp_format - format for :timestamp columns, defaults to YYYY-MM-DD HH24:MI:SS:FF6 # * :nls_timestamp_tz_format # * :nls_time_format # * :nls_time_tz_format # class OracleEnhancedAdapter < AbstractAdapter ## # :singleton-method: # By default, the OracleEnhancedAdapter will consider all columns of type NUMBER(1) # as boolean. If you wish to disable this emulation you can add the following line # to your initializer file: # # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_booleans = false cattr_accessor :emulate_booleans self.emulate_booleans = true ## # :singleton-method: # By default, the OracleEnhancedAdapter will typecast all columns of type DATE # to Time or DateTime (if value is out of Time value range) value. # If you wish that DATE values with hour, minutes and seconds equal to 0 are typecasted # to Date then you can add the following line to your initializer file: # # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_dates = true # # As this option can have side effects when unnecessary typecasting is done it is recommended # that Date columns are explicily defined with +set_date_columns+ method. cattr_accessor :emulate_dates self.emulate_dates = false ## # :singleton-method: # OracleEnhancedAdapter will use the default tablespace, but if you want specific types of # objects to go into specific tablespaces, specify them like this in an initializer: # # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.default_tablespaces = # {:clob => 'TS_LOB', :blob => 'TS_LOB', :index => 'TS_INDEX', :table => 'TS_DATA'} # # Using the :tablespace option where available (e.g create_table) will take precedence # over these settings. cattr_accessor :default_tablespaces self.default_tablespaces={} ## # :singleton-method: # By default, the OracleEnhancedAdapter will typecast all columns of type DATE # to Time or DateTime (if value is out of Time value range) value. # If you wish that DATE columns with "date" in their name (e.g. "creation_date") are typecasted # to Date then you can add the following line to your initializer file: # # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_dates_by_column_name = true # # As this option can have side effects when unnecessary typecasting is done it is recommended # that Date columns are explicily defined with +set_date_columns+ method. cattr_accessor :emulate_dates_by_column_name self.emulate_dates_by_column_name = false # Check column name to identify if it is Date (and not Time) column. # Is used if +emulate_dates_by_column_name+ option is set to +true+. # Override this method definition in initializer file if different Date column recognition is needed. def self.is_date_column?(name, table_name = nil) name =~ /(^|_)date(_|$)/i end # instance method uses at first check if column type defined at class level def is_date_column?(name, table_name = nil) #:nodoc: case get_type_for_column(table_name, name) when nil self.class.is_date_column?(name, table_name) when :date true else false end end ## # :singleton-method: # By default, the OracleEnhancedAdapter will typecast all columns of type NUMBER # (without precision or scale) to Float or BigDecimal value. # If you wish that NUMBER columns with name "id" or that end with "_id" are typecasted # to Integer then you can add the following line to your initializer file: # # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_integers_by_column_name = true cattr_accessor :emulate_integers_by_column_name self.emulate_integers_by_column_name = false # Check column name to identify if it is Integer (and not Float or BigDecimal) column. # Is used if +emulate_integers_by_column_name+ option is set to +true+. # Override this method definition in initializer file if different Integer column recognition is needed. def self.is_integer_column?(name, table_name = nil) name =~ /(^|_)id$/i end ## # :singleton-method: # If you wish that CHAR(1), VARCHAR2(1) columns or VARCHAR2 columns with FLAG or YN at the end of their name # are typecasted to booleans then you can add the following line to your initializer file: # # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_booleans_from_strings = true cattr_accessor :emulate_booleans_from_strings self.emulate_booleans_from_strings = false # Check column name to identify if it is boolean (and not String) column. # Is used if +emulate_booleans_from_strings+ option is set to +true+. # Override this method definition in initializer file if different boolean column recognition is needed. def self.is_boolean_column?(name, field_type, table_name = nil) return true if ["CHAR(1)","VARCHAR2(1)"].include?(field_type) field_type =~ /^VARCHAR2/ && (name =~ /_flag$/i || name =~ /_yn$/i) end # How boolean value should be quoted to String. # Used if +emulate_booleans_from_strings+ option is set to +true+. def self.boolean_to_string(bool) bool ? "Y" : "N" end ## # :singleton-method: # Specify non-default date format that should be used when assigning string values to :date columns, e.g.: # # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.string_to_date_format = ā€œ%d.%m.%Yā€ cattr_accessor :string_to_date_format self.string_to_date_format = nil ## # :singleton-method: # Specify non-default time format that should be used when assigning string values to :datetime columns, e.g.: # # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.string_to_time_format = ā€œ%d.%m.%Y %H:%M:%Sā€ cattr_accessor :string_to_time_format self.string_to_time_format = nil class StatementPool include Enumerable def initialize(connection, max = 300) @connection = connection @max = max @cache = {} end def each(&block); @cache.each(&block); end def key?(key); @cache.key?(key); end def [](key); @cache[key]; end def length; @cache.length; end def delete(key); @cache.delete(key); end def []=(sql, key) while @max <= @cache.size @cache.shift.last.close end @cache[sql] = key end def clear @cache.values.each do |cursor| cursor.close end @cache.clear end end def initialize(connection, logger, config) #:nodoc: super(connection, logger) @quoted_column_names, @quoted_table_names = {}, {} @config = config @statements = StatementPool.new(connection, config.fetch(:statement_limit) { 250 }) @enable_dbms_output = false @visitor = Arel::Visitors::Oracle.new self if defined?(Arel::Visitors::Oracle) end def self.visitor_for(pool) # :nodoc: Arel::Visitors::Oracle.new(pool) end ADAPTER_NAME = 'OracleEnhanced'.freeze def adapter_name #:nodoc: ADAPTER_NAME end def supports_migrations? #:nodoc: true end def supports_primary_key? #:nodoc: true end def supports_savepoints? #:nodoc: true end #:stopdoc: DEFAULT_NLS_PARAMETERS = { :nls_calendar => nil, :nls_comp => nil, :nls_currency => nil, :nls_date_format => 'YYYY-MM-DD HH24:MI:SS', :nls_date_language => nil, :nls_dual_currency => nil, :nls_iso_currency => nil, :nls_language => nil, :nls_length_semantics => 'CHAR', :nls_nchar_conv_excp => nil, :nls_numeric_characters => nil, :nls_sort => nil, :nls_territory => nil, :nls_timestamp_format => 'YYYY-MM-DD HH24:MI:SS:FF6', :nls_timestamp_tz_format => nil, :nls_time_format => nil, :nls_time_tz_format => nil } #:stopdoc: NATIVE_DATABASE_TYPES = { :primary_key => "NUMBER(38) NOT NULL PRIMARY KEY", :string => { :name => "VARCHAR2", :limit => 255 }, :text => { :name => "CLOB" }, :integer => { :name => "NUMBER", :limit => 38 }, :float => { :name => "NUMBER" }, :decimal => { :name => "DECIMAL" }, :datetime => { :name => "DATE" }, # changed to native TIMESTAMP type # :timestamp => { :name => "DATE" }, :timestamp => { :name => "TIMESTAMP" }, :time => { :name => "DATE" }, :date => { :name => "DATE" }, :binary => { :name => "BLOB" }, :boolean => { :name => "NUMBER", :limit => 1 }, :raw => { :name => "RAW", :limit => 2000 } } # if emulate_booleans_from_strings then store booleans in VARCHAR2 NATIVE_DATABASE_TYPES_BOOLEAN_STRINGS = NATIVE_DATABASE_TYPES.dup.merge( :boolean => { :name => "VARCHAR2", :limit => 1 } ) #:startdoc: def native_database_types #:nodoc: emulate_booleans_from_strings ? NATIVE_DATABASE_TYPES_BOOLEAN_STRINGS : NATIVE_DATABASE_TYPES end # maximum length of Oracle identifiers IDENTIFIER_MAX_LENGTH = 30 def table_alias_length #:nodoc: IDENTIFIER_MAX_LENGTH end # the maximum length of a table name def table_name_length IDENTIFIER_MAX_LENGTH end # the maximum length of a column name def column_name_length IDENTIFIER_MAX_LENGTH end # the maximum length of an index name def index_name_length IDENTIFIER_MAX_LENGTH end # the maximum length of a sequence name def sequence_name_length IDENTIFIER_MAX_LENGTH end # To avoid ORA-01795: maximum number of expressions in a list is 1000 # tell ActiveRecord to limit us to 1000 ids at a time def in_clause_length 1000 end alias ids_in_list_limit in_clause_length # QUOTING ================================================== # # see: abstract/quoting.rb def quote_column_name(name) #:nodoc: name = name.to_s @quoted_column_names[name] ||= begin # if only valid lowercase column characters in name if name =~ /\A[a-z][a-z_0-9\$#]*\Z/ "\"#{name.upcase}\"" else # remove double quotes which cannot be used inside quoted identifier "\"#{name.gsub('"', '')}\"" end end end # This method is used in add_index to identify either column name (which is quoted) # or function based index (in which case function expression is not quoted) def quote_column_name_or_expression(name) #:nodoc: name = name.to_s case name # if only valid lowercase column characters in name when /^[a-z][a-z_0-9\$#]*$/ "\"#{name.upcase}\"" when /^[a-z][a-z_0-9\$#\-]*$/i "\"#{name}\"" # if other characters present then assume that it is expression # which should not be quoted else name end end # Names must be from 1 to 30 bytes long with these exceptions: # * Names of databases are limited to 8 bytes. # * Names of database links can be as long as 128 bytes. # # Nonquoted identifiers cannot be Oracle Database reserved words # # Nonquoted identifiers must begin with an alphabetic character from # your database character set # # Nonquoted identifiers can contain only alphanumeric characters from # your database character set and the underscore (_), dollar sign ($), # and pound sign (#). Database links can also contain periods (.) and # "at" signs (@). Oracle strongly discourages you from using $ and # in # nonquoted identifiers. NONQUOTED_OBJECT_NAME = /[A-Za-z][A-z0-9$#]{0,29}/ NONQUOTED_DATABASE_LINK = /[A-Za-z][A-z0-9$#\.@]{0,127}/ VALID_TABLE_NAME = /\A(?:#{NONQUOTED_OBJECT_NAME}\.)?#{NONQUOTED_OBJECT_NAME}(?:@#{NONQUOTED_DATABASE_LINK})?\Z/ # unescaped table name should start with letter and # contain letters, digits, _, $ or # # can be prefixed with schema name # CamelCase table names should be quoted def self.valid_table_name?(name) #:nodoc: name = name.to_s name =~ VALID_TABLE_NAME && !(name =~ /[A-Z]/ && name =~ /[a-z]/) ? true : false end def quote_table_name(name) #:nodoc: name = name.to_s @quoted_table_names[name] ||= name.split('.').map{|n| n.split('@').map{|m| quote_column_name(m)}.join('@')}.join('.') end def quote_string(s) #:nodoc: s.gsub(/'/, "''") end def quote(value, column = nil) #:nodoc: if value && column case column.type when :text, :binary %Q{empty_#{ column.sql_type.downcase rescue 'blob' }()} # NLS_DATE_FORMAT independent TIMESTAMP support when :timestamp quote_timestamp_with_to_timestamp(value) # NLS_DATE_FORMAT independent DATE support when :date, :time, :datetime quote_date_with_to_date(value) when :raw quote_raw(value) when :string # NCHAR and NVARCHAR2 literals should be quoted with N'...'. # Read directly instance variable as otherwise migrations with table column default values are failing # as migrations pass ColumnDefinition object to this method. # Check if instance variable is defined to avoid warnings about accessing undefined instance variable. column.instance_variable_defined?('@nchar') && column.instance_variable_get('@nchar') ? 'N' << super : super else super end elsif value.acts_like?(:date) quote_date_with_to_date(value) elsif value.acts_like?(:time) value.to_i == value.to_f ? quote_date_with_to_date(value) : quote_timestamp_with_to_timestamp(value) else super end end def quoted_true #:nodoc: return "'#{self.class.boolean_to_string(true)}'" if emulate_booleans_from_strings "1" end def quoted_false #:nodoc: return "'#{self.class.boolean_to_string(false)}'" if emulate_booleans_from_strings "0" end def quote_date_with_to_date(value) #:nodoc: # should support that composite_primary_keys gem will pass date as string value = quoted_date(value) if value.acts_like?(:date) || value.acts_like?(:time) "TO_DATE('#{value}','YYYY-MM-DD HH24:MI:SS')" end # Encode a string or byte array as string of hex codes def self.encode_raw(value) # When given a string, convert to a byte array. value = value.unpack('C*') if value.is_a?(String) value.map { |x| "%02X" % x }.join end # quote encoded raw value def quote_raw(value) #:nodoc: "'#{self.class.encode_raw(value)}'" end def quote_timestamp_with_to_timestamp(value) #:nodoc: # add up to 9 digits of fractional seconds to inserted time value = "#{quoted_date(value)}:#{("%.6f"%value.to_f).split('.')[1]}" if value.acts_like?(:time) "TO_TIMESTAMP('#{value}','YYYY-MM-DD HH24:MI:SS:FF6')" end # Cast a +value+ to a type that the database understands. def type_cast(value, column) case value when true, false if emulate_booleans_from_strings || column && column.type == :string self.class.boolean_to_string(value) else value ? 1 : 0 end when Date, Time if value.acts_like?(:time) zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal value.respond_to?(zone_conversion_method) ? value.send(zone_conversion_method) : value else value end else super end end # CONNECTION MANAGEMENT ==================================== # # If SQL statement fails due to lost connection then reconnect # and retry SQL statement if autocommit mode is enabled. # By default this functionality is disabled. attr_reader :auto_retry #:nodoc: @auto_retry = false def auto_retry=(value) #:nodoc: @auto_retry = value @connection.auto_retry = value if @connection end # return raw OCI8 or JDBC connection def raw_connection @connection.raw_connection end # Returns true if the connection is active. def active? #:nodoc: # Pings the connection to check if it's still good. Note that an # #active? method is also available, but that simply returns the # last known state, which isn't good enough if the connection has # gone stale since the last use. @connection.ping rescue OracleEnhancedConnectionException false end # Reconnects to the database. def reconnect! #:nodoc: clear_cache! @connection.reset! rescue OracleEnhancedConnectionException => e @logger.warn "#{adapter_name} automatic reconnection failed: #{e.message}" if @logger end def reset! clear_cache! super end # Disconnects from the database. def disconnect! #:nodoc: clear_cache! @connection.logoff rescue nil end # DATABASE STATEMENTS ====================================== # # see: abstract/database_statements.rb # Executes a SQL statement def execute(sql, name = nil) log(sql, name) { @connection.exec(sql) } end def substitute_at(column, index) Arel.sql(":a#{index + 1}") end def clear_cache! @statements.clear end def exec_query(sql, name = 'SQL', binds = []) log(sql, name, binds) do cursor = nil cached = false if binds.empty? cursor = @connection.prepare(sql) else unless @statements.key? sql @statements[sql] = @connection.prepare(sql) end cursor = @statements[sql] binds.each_with_index do |bind, i| col, val = bind cursor.bind_param(i + 1, type_cast(val, col), col && col.type) end cached = true end cursor.exec if name == 'EXPLAIN' res = true else columns = cursor.get_col_names.map do |col_name| @connection.oracle_downcase(col_name) end rows = [] fetch_options = {:get_lob_value => (name != 'Writable Large Object')} while row = cursor.fetch(fetch_options) rows << row end res = ActiveRecord::Result.new(columns, rows) end cursor.close unless cached res end end def supports_statement_cache? true end def supports_explain? true end def explain(arel, binds = []) sql = "EXPLAIN PLAN FOR #{to_sql(arel)}" return if sql =~ /FROM all_/ if ORACLE_ENHANCED_CONNECTION == :jdbc exec_query(sql, 'EXPLAIN', binds) else exec_query(sql, 'EXPLAIN') end select_values("SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY)", 'EXPLAIN').join("\n") end # Returns an array of arrays containing the field values. # Order is the same as that returned by #columns. def select_rows(sql, name = nil) # last parameter indicates to return also column list result = columns = nil log(sql, name) do result, columns = @connection.select(sql, name, true) end result.map{ |v| columns.map{|c| v[c]} } end # Executes an INSERT statement and returns the new record's ID def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: # if primary key value is already prefetched from sequence # or if there is no primary key if id_value || pk.nil? execute(sql, name) return id_value end sql_with_returning = sql + @connection.returning_clause(quote_column_name(pk)) log(sql, name) do @connection.exec_with_returning(sql_with_returning) end end protected :insert_sql # New method in ActiveRecord 3.1 # Will add RETURNING clause in case of trigger generated primary keys def sql_for_insert(sql, pk, id_value, sequence_name, binds) unless id_value || pk.nil? || (defined?(CompositePrimaryKeys) && pk.kind_of?(CompositePrimaryKeys::CompositeKeys)) sql = "#{sql} RETURNING #{quote_column_name(pk)} INTO :returning_id" returning_id_col = OracleEnhancedColumn.new("returning_id", nil, "number", true, "dual", :integer, true, true) (binds = binds.dup) << [returning_id_col, nil] end [sql, binds] end # New method in ActiveRecord 3.1 def exec_insert(sql, name, binds) log(sql, name, binds) do returning_id_col = returning_id_index = nil cursor = if @statements.key?(sql) @statements[sql] else @statements[sql] = @connection.prepare(sql) end binds.each_with_index do |bind, i| col, val = bind if col.returning_id? returning_id_col = [col] returning_id_index = i + 1 cursor.bind_returning_param(returning_id_index, Integer) else cursor.bind_param(i + 1, type_cast(val, col), col && col.type) end end cursor.exec_update rows = [] if returning_id_index returning_id = cursor.get_returning_param(returning_id_index, Integer) rows << [returning_id] end ActiveRecord::Result.new(returning_id_col || [], rows) end end # New method in ActiveRecord 3.1 def exec_update(sql, name, binds) log(sql, name, binds) do cached = false if binds.empty? cursor = @connection.prepare(sql) else cursor = if @statements.key?(sql) @statements[sql] else @statements[sql] = @connection.prepare(sql) end binds.each_with_index do |bind, i| col, val = bind cursor.bind_param(i + 1, type_cast(val, col), col && col.type) end cached = true end res = cursor.exec_update cursor.close unless cached res end end alias :exec_delete :exec_update # use in set_sequence_name to avoid fetching primary key value from sequence AUTOGENERATED_SEQUENCE_NAME = 'autogenerated'.freeze # Returns the next sequence value from a sequence generator. Not generally # called directly; used by ActiveRecord to get the next primary key value # when inserting a new database record (see #prefetch_primary_key?). def next_sequence_value(sequence_name) # if sequence_name is set to :autogenerated then it means that primary key will be populated by trigger return nil if sequence_name == AUTOGENERATED_SEQUENCE_NAME # call directly connection method to avoid prepared statement which causes fetching of next sequence value twice @connection.select_value("SELECT #{quote_table_name(sequence_name)}.NEXTVAL FROM dual") end def begin_db_transaction #:nodoc: @connection.autocommit = false end def commit_db_transaction #:nodoc: @connection.commit ensure @connection.autocommit = true end def rollback_db_transaction #:nodoc: @connection.rollback ensure @connection.autocommit = true end def create_savepoint #:nodoc: execute("SAVEPOINT #{current_savepoint_name}") end def rollback_to_savepoint #:nodoc: execute("ROLLBACK TO #{current_savepoint_name}") end def release_savepoint #:nodoc: # there is no RELEASE SAVEPOINT statement in Oracle end def add_limit_offset!(sql, options) #:nodoc: # added to_i for limit and offset to protect from SQL injection offset = (options[:offset] || 0).to_i limit = options[:limit] limit = limit.is_a?(String) && limit.blank? ? nil : limit && limit.to_i if limit && offset > 0 sql.replace "SELECT * FROM (SELECT raw_sql_.*, ROWNUM raw_rnum_ FROM (#{sql}) raw_sql_ WHERE ROWNUM <= #{offset+limit}) WHERE raw_rnum_ > #{offset}" elsif limit sql.replace "SELECT * FROM (#{sql}) WHERE ROWNUM <= #{limit}" elsif offset > 0 sql.replace "SELECT * FROM (SELECT raw_sql_.*, ROWNUM raw_rnum_ FROM (#{sql}) raw_sql_) WHERE raw_rnum_ > #{offset}" end end @@do_not_prefetch_primary_key = {} # Returns true for Oracle adapter (since Oracle requires primary key # values to be pre-fetched before insert). See also #next_sequence_value. def prefetch_primary_key?(table_name = nil) return true if table_name.nil? table_name = table_name.to_s do_not_prefetch = @@do_not_prefetch_primary_key[table_name] if do_not_prefetch.nil? owner, desc_table_name, db_link = @connection.describe(table_name) @@do_not_prefetch_primary_key[table_name] = do_not_prefetch = !has_primary_key?(table_name, owner, desc_table_name, db_link) || has_primary_key_trigger?(table_name, owner, desc_table_name, db_link) end !do_not_prefetch end # used just in tests to clear prefetch primary key flag for all tables def clear_prefetch_primary_key #:nodoc: @@do_not_prefetch_primary_key = {} end # Returns default sequence name for table. # Will take all or first 26 characters of table name and append _seq suffix def default_sequence_name(table_name, primary_key = nil) # TODO: remove schema prefix if present before truncating # truncate table name if necessary to fit in max length of identifier "#{table_name.to_s[0,IDENTIFIER_MAX_LENGTH-4]}_seq" end # Inserts the given fixture into the table. Overridden to properly handle lobs. def insert_fixture(fixture, table_name) #:nodoc: super if ActiveRecord::Base.pluralize_table_names klass = table_name.singularize.camelize else klass = table_name.camelize end klass = klass.constantize rescue nil if klass.respond_to?(:ancestors) && klass.ancestors.include?(ActiveRecord::Base) write_lobs(table_name, klass, fixture, klass.lob_columns) end end # Writes LOB values from attributes for specified columns def write_lobs(table_name, klass, attributes, columns) #:nodoc: # is class with composite primary key> is_with_cpk = klass.respond_to?(:composite?) && klass.composite? if is_with_cpk id = klass.primary_key.map {|pk| attributes[pk.to_s] } else id = quote(attributes[klass.primary_key]) end columns.each do |col| value = attributes[col.name] # changed sequence of next two lines - should check if value is nil before converting to yaml next if value.nil? || (value == '') value = value.to_yaml if col.text? && klass.serialized_attributes[col.name] uncached do sql = is_with_cpk ? "SELECT #{quote_column_name(col.name)} FROM #{quote_table_name(table_name)} WHERE #{klass.composite_where_clause(id)} FOR UPDATE" : "SELECT #{quote_column_name(col.name)} FROM #{quote_table_name(table_name)} WHERE #{quote_column_name(klass.primary_key)} = #{id} FOR UPDATE" unless lob_record = select_one(sql, 'Writable Large Object') raise ActiveRecord::RecordNotFound, "statement #{sql} returned no rows" end lob = lob_record[col.name] @connection.write_lob(lob, value.to_s, col.type == :binary) end end end # Current database name def current_database select_value("SELECT SYS_CONTEXT('userenv', 'db_name') FROM dual") end # Current database session user def current_user select_value("SELECT SYS_CONTEXT('userenv', 'session_user') FROM dual") end # Default tablespace name of current user def default_tablespace select_value("SELECT LOWER(default_tablespace) FROM user_users WHERE username = SYS_CONTEXT('userenv', 'session_user')") end def tables(name = nil) #:nodoc: select_values( "SELECT DECODE(table_name, UPPER(table_name), LOWER(table_name), table_name) FROM all_tables WHERE owner = SYS_CONTEXT('userenv', 'session_user') AND secondary = 'N'", name) end # Will return true if database object exists (to be able to use also views and synonyms for ActiveRecord models) def table_exists?(table_name) (owner, table_name, db_link) = @connection.describe(table_name) true rescue false end def materialized_views #:nodoc: select_values("SELECT LOWER(mview_name) FROM all_mviews WHERE owner = SYS_CONTEXT('userenv', 'session_user')") end cattr_accessor :all_schema_indexes #:nodoc: # This method selects all indexes at once, and caches them in a class variable. # Subsequent index calls get them from the variable, without going to the DB. def indexes(table_name, name = nil) #:nodoc: (owner, table_name, db_link) = @connection.describe(table_name) unless all_schema_indexes default_tablespace_name = default_tablespace result = select_all(<<-SQL.strip.gsub(/\s+/, ' ')) SELECT LOWER(i.table_name) AS table_name, LOWER(i.index_name) AS index_name, i.uniqueness, i.index_type, i.ityp_owner, i.ityp_name, i.parameters, LOWER(i.tablespace_name) AS tablespace_name, LOWER(c.column_name) AS column_name, e.column_expression, atc.virtual_column FROM all_indexes#{db_link} i JOIN all_ind_columns#{db_link} c ON c.index_name = i.index_name AND c.index_owner = i.owner LEFT OUTER JOIN all_ind_expressions#{db_link} e ON e.index_name = i.index_name AND e.index_owner = i.owner AND e.column_position = c.column_position LEFT OUTER JOIN all_tab_cols#{db_link} atc ON i.table_name = atc.table_name AND c.column_name = atc.column_name AND i.owner = atc.owner AND atc.hidden_column = 'NO' WHERE i.owner = '#{owner}' AND i.table_owner = '#{owner}' AND NOT EXISTS (SELECT uc.index_name FROM all_constraints uc WHERE uc.index_name = i.index_name AND uc.owner = i.owner AND uc.constraint_type = 'P') ORDER BY i.index_name, c.column_position SQL current_index = nil self.all_schema_indexes = [] result.each do |row| # have to keep track of indexes because above query returns dups # there is probably a better query we could figure out if current_index != row['index_name'] statement_parameters = nil if row['index_type'] == 'DOMAIN' && row['ityp_owner'] == 'CTXSYS' && row['ityp_name'] == 'CONTEXT' procedure_name = default_datastore_procedure(row['index_name']) source = select_values(<<-SQL).join SELECT text FROM all_source#{db_link} WHERE owner = '#{owner}' AND name = '#{procedure_name.upcase}' ORDER BY line SQL if source =~ /-- add_context_index_parameters (.+)\n/ statement_parameters = $1 end end all_schema_indexes << OracleEnhancedIndexDefinition.new(row['table_name'], row['index_name'], row['uniqueness'] == "UNIQUE", row['index_type'] == 'DOMAIN' ? "#{row['ityp_owner']}.#{row['ityp_name']}" : nil, row['parameters'], statement_parameters, row['tablespace_name'] == default_tablespace_name ? nil : row['tablespace_name'], []) current_index = row['index_name'] end # Functional index columns and virtual columns both get stored as column expressions, # but re-creating a virtual column index as an expression (instead of using the virtual column's name) # results in a ORA-54018 error. Thus, we only want the column expression value returned # when the column is not virtual. if row['column_expression'] && row['virtual_column'] != 'YES' all_schema_indexes.last.columns << row['column_expression'] else all_schema_indexes.last.columns << row['column_name'].downcase end end end # Return the indexes just for the requested table, since AR is structured that way table_name = table_name.downcase all_schema_indexes.select{|i| i.table == table_name} end @@ignore_table_columns = nil #:nodoc: # set ignored columns for table def ignore_table_columns(table_name, *args) #:nodoc: @@ignore_table_columns ||= {} @@ignore_table_columns[table_name] ||= [] @@ignore_table_columns[table_name] += args.map{|a| a.to_s.downcase} @@ignore_table_columns[table_name].uniq! end def ignored_table_columns(table_name) #:nodoc: @@ignore_table_columns ||= {} @@ignore_table_columns[table_name] end # used just in tests to clear ignored table columns def clear_ignored_table_columns #:nodoc: @@ignore_table_columns = nil end @@table_column_type = nil #:nodoc: # set explicit type for specified table columns def set_type_for_columns(table_name, column_type, *args) #:nodoc: @@table_column_type ||= {} @@table_column_type[table_name] ||= {} args.each do |col| @@table_column_type[table_name][col.to_s.downcase] = column_type end end def get_type_for_column(table_name, column_name) #:nodoc: @@table_column_type && @@table_column_type[table_name] && @@table_column_type[table_name][column_name.to_s.downcase] end # used just in tests to clear column data type definitions def clear_types_for_columns #:nodoc: @@table_column_type = nil end # check if table has primary key trigger with _pkt suffix def has_primary_key_trigger?(table_name, owner = nil, desc_table_name = nil, db_link = nil) (owner, desc_table_name, db_link) = @connection.describe(table_name) unless owner trigger_name = default_trigger_name(table_name).upcase pkt_sql = <<-SQL SELECT trigger_name FROM all_triggers#{db_link} WHERE owner = '#{owner}' AND trigger_name = '#{trigger_name}' AND table_owner = '#{owner}' AND table_name = '#{desc_table_name}' AND status = 'ENABLED' SQL select_value(pkt_sql, 'Primary Key Trigger') ? true : false end ## # :singleton-method: # Cache column description between requests. # Could be used in development environment to avoid selecting table columns from data dictionary tables for each request. # This can speed up request processing in development mode if development database is not on local computer. # # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.cache_columns = true cattr_accessor :cache_columns self.cache_columns = false def columns(table_name, name = nil) #:nodoc: if @@cache_columns @@columns_cache ||= {} @@columns_cache[table_name] ||= columns_without_cache(table_name, name) else columns_without_cache(table_name, name) end end def columns_without_cache(table_name, name = nil) #:nodoc: table_name = table_name.to_s # get ignored_columns by original table name ignored_columns = ignored_table_columns(table_name) (owner, desc_table_name, db_link) = @connection.describe(table_name) # reset do_not_prefetch_primary_key cache for this table @@do_not_prefetch_primary_key[table_name] = nil table_cols = <<-SQL.strip.gsub(/\s+/, ' ') SELECT column_name AS name, data_type AS sql_type, data_default, nullable, virtual_column, hidden_column, DECODE(data_type, 'NUMBER', data_precision, 'FLOAT', data_precision, 'VARCHAR2', DECODE(char_used, 'C', char_length, data_length), 'RAW', DECODE(char_used, 'C', char_length, data_length), 'CHAR', DECODE(char_used, 'C', char_length, data_length), NULL) AS limit, DECODE(data_type, 'NUMBER', data_scale, NULL) AS scale FROM all_tab_cols#{db_link} WHERE owner = '#{owner}' AND table_name = '#{desc_table_name}' AND hidden_column = 'NO' ORDER BY column_id SQL # added deletion of ignored columns select_all(table_cols, name).delete_if do |row| ignored_columns && ignored_columns.include?(row['name'].downcase) end.map do |row| limit, scale = row['limit'], row['scale'] if limit || scale row['sql_type'] += "(#{(limit || 38).to_i}" + ((scale = scale.to_i) > 0 ? ",#{scale})" : ")") end is_virtual = row['virtual_column']=='YES' # clean up odd default spacing from Oracle if row['data_default'] && !is_virtual row['data_default'].sub!(/^(.*?)\s*$/, '\1') # If a default contains a newline these cleanup regexes need to # match newlines. row['data_default'].sub!(/^'(.*)'$/m, '\1') row['data_default'] = nil if row['data_default'] =~ /^(null|empty_[bc]lob\(\))$/i end OracleEnhancedColumn.new(oracle_downcase(row['name']), row['data_default'], row['sql_type'], row['nullable'] == 'Y', # pass table name for table specific column definitions table_name, # pass column type if specified in class definition get_type_for_column(table_name, oracle_downcase(row['name'])), is_virtual) end end # used just in tests to clear column cache def clear_columns_cache #:nodoc: @@columns_cache = nil @@pk_and_sequence_for_cache = nil end # used in migrations to clear column cache for specified table def clear_table_columns_cache(table_name) if @@cache_columns @@columns_cache ||= {} @@columns_cache[table_name.to_s] = nil end end ## # :singleton-method: # Specify default sequence start with value (by default 10000 if not explicitly set), e.g.: # # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.default_sequence_start_value = 1 cattr_accessor :default_sequence_start_value self.default_sequence_start_value = 10000 # Find a table's primary key and sequence. # *Note*: Only primary key is implemented - sequence will be nil. def pk_and_sequence_for(table_name, owner=nil, desc_table_name=nil, db_link=nil) #:nodoc: if @@cache_columns @@pk_and_sequence_for_cache ||= {} if @@pk_and_sequence_for_cache.key?(table_name) @@pk_and_sequence_for_cache[table_name] else @@pk_and_sequence_for_cache[table_name] = pk_and_sequence_for_without_cache(table_name, owner, desc_table_name, db_link) end else pk_and_sequence_for_without_cache(table_name, owner, desc_table_name, db_link) end end def pk_and_sequence_for_without_cache(table_name, owner=nil, desc_table_name=nil, db_link=nil) #:nodoc: (owner, desc_table_name, db_link) = @connection.describe(table_name) unless owner # changed back from user_constraints to all_constraints for consistency pks = select_values(<<-SQL.strip.gsub(/\s+/, ' '), 'Primary Key') SELECT cc.column_name FROM all_constraints#{db_link} c, all_cons_columns#{db_link} cc WHERE c.owner = '#{owner}' AND c.table_name = '#{desc_table_name}' AND c.constraint_type = 'P' AND cc.owner = c.owner AND cc.constraint_name = c.constraint_name SQL # only support single column keys pks.size == 1 ? [oracle_downcase(pks.first), nil] : nil end # Returns just a table's primary key def primary_key(table_name) pk_and_sequence = pk_and_sequence_for(table_name) pk_and_sequence && pk_and_sequence.first end def has_primary_key?(table_name, owner=nil, desc_table_name=nil, db_link=nil) #:nodoc: !pk_and_sequence_for(table_name, owner, desc_table_name, db_link).nil? end # SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause. # # Oracle requires the ORDER BY columns to be in the SELECT list for DISTINCT # queries. However, with those columns included in the SELECT DISTINCT list, you # won't actually get a distinct list of the column you want (presuming the column # has duplicates with multiple values for the ordered-by columns. So we use the # FIRST_VALUE function to get a single (first) value for each column, effectively # making every row the same. # # distinct("posts.id", "posts.created_at desc") def distinct(columns, order_by) #:nodoc: return "DISTINCT #{columns}" if order_by.blank? # construct a valid DISTINCT clause, ie. one that includes the ORDER BY columns, using # FIRST_VALUE such that the inclusion of these columns doesn't invalidate the DISTINCT order_columns = if order_by.is_a?(String) order_by.split(',').map { |s| s.strip }.reject(&:blank?) else # in latest ActiveRecord versions order_by is already Array order_by end order_columns = order_columns.zip((0...order_columns.size).to_a).map do |c, i| # remove any ASC/DESC modifiers value = c =~ /^(.+)\s+(ASC|DESC)\s*$/i ? $1 : c "FIRST_VALUE(#{value}) OVER (PARTITION BY #{columns} ORDER BY #{c}) AS alias_#{i}__" end sql = "DISTINCT #{columns}, " sql << order_columns * ", " end def temporary_table?(table_name) #:nodoc: select_value("SELECT temporary FROM user_tables WHERE table_name = '#{table_name.upcase}'") == 'Y' end # ORDER BY clause for the passed order option. # # Uses column aliases as defined by #distinct. # # In Rails 3.x this method is moved to Arel def add_order_by_for_association_limiting!(sql, options) #:nodoc: return sql if options[:order].blank? order = options[:order].split(',').collect { |s| s.strip }.reject(&:blank?) order.map! {|s| $1 if s =~ / (.*)/} order = order.zip((0...order.size).to_a).map { |s,i| "alias_#{i}__ #{s}" }.join(', ') sql << " ORDER BY #{order}" end # construct additional wrapper subquery if select.offset is used to avoid generation of invalid subquery # ... IN ( SELECT * FROM ( SELECT raw_sql_.*, rownum raw_rnum_ FROM ( ... ) raw_sql_ ) WHERE raw_rnum_ > ... ) def join_to_update(update, select) #:nodoc: if select.offset subsubselect = select.clone subsubselect.projections = [update.key] subselect = Arel::SelectManager.new(select.engine) subselect.project Arel.sql(quote_column_name update.key.name) subselect.from subsubselect.as('alias_join_to_update') update.where update.key.in(subselect) else super end end protected def translate_exception(exception, message) #:nodoc: case @connection.error_code(exception) when 1 RecordNotUnique.new(message, exception) when 2291 InvalidForeignKey.new(message, exception) else super end end private def select(sql, name = nil, binds = []) if ActiveRecord.const_defined?(:Result) exec_query(sql, name, binds).to_a else log(sql, name) do @connection.select(sql, name, false) end end end def oracle_downcase(column_name) @connection.oracle_downcase(column_name) end def compress_lines(string, join_with = "\n") string.split($/).map { |line| line.strip }.join(join_with) end public # DBMS_OUTPUT ============================================= # # PL/SQL in Oracle uses dbms_output for logging print statements # These methods stick that output into the Rails log so Ruby and PL/SQL # code can can be debugged together in a single application # Maximum DBMS_OUTPUT buffer size DBMS_OUTPUT_BUFFER_SIZE = 10000 # can be 1-1000000 # Turn DBMS_Output logging on def enable_dbms_output set_dbms_output_plsql_connection @enable_dbms_output = true plsql(:dbms_output).sys.dbms_output.enable(DBMS_OUTPUT_BUFFER_SIZE) end # Turn DBMS_Output logging off def disable_dbms_output set_dbms_output_plsql_connection @enable_dbms_output = false plsql(:dbms_output).sys.dbms_output.disable end # Is DBMS_Output logging enabled? def dbms_output_enabled? @enable_dbms_output end protected def log(sql, name, binds = nil) #:nodoc: if binds super sql, name, binds else super sql, name end ensure log_dbms_output if dbms_output_enabled? end private def set_dbms_output_plsql_connection raise OracleEnhancedConnectionException, "ruby-plsql gem is required for logging DBMS output" unless self.respond_to?(:plsql) # do not reset plsql connection if it is the same (as resetting will clear PL/SQL metadata cache) unless plsql(:dbms_output).connection && plsql(:dbms_output).connection.raw_connection == raw_connection plsql(:dbms_output).connection = raw_connection end end def log_dbms_output while true do result = plsql(:dbms_output).sys.dbms_output.get_line(:line => '', :status => 0) break unless result[:status] == 0 @logger.debug "DBMS_OUTPUT: #{result[:line]}" if @logger end end end end end # Added LOB writing callback for sessions stored in database # Otherwise it is not working as Session class is defined before OracleAdapter is loaded in Rails 2.0 if defined?(CGI::Session::ActiveRecordStore::Session) if !CGI::Session::ActiveRecordStore::Session.respond_to?(:after_save_callback_chain) || CGI::Session::ActiveRecordStore::Session.after_save_callback_chain.detect{|cb| cb.method == :enhanced_write_lobs}.nil? #:stopdoc: class CGI::Session::ActiveRecordStore::Session after_save :enhanced_write_lobs end #:startdoc: end end # Implementation of standard schema definition statements and extensions for schema definition require 'active_record/connection_adapters/oracle_enhanced_schema_statements' require 'active_record/connection_adapters/oracle_enhanced_schema_statements_ext' # Extensions for schema definition require 'active_record/connection_adapters/oracle_enhanced_schema_definitions' # Extensions for context index definition require 'active_record/connection_adapters/oracle_enhanced_context_index' # Load custom create, update, delete methods functionality require 'active_record/connection_adapters/oracle_enhanced_procedures' # Load additional methods for composite_primary_keys support require 'active_record/connection_adapters/oracle_enhanced_cpk' # Load patch for dirty tracking methods require 'active_record/connection_adapters/oracle_enhanced_dirty' # Load rake tasks definitions begin require 'active_record/connection_adapters/oracle_enhanced_tasks' rescue LoadError end if defined?(Rails) || defined?(RAILS_ROOT) # Patches and enhancements for schema dumper require 'active_record/connection_adapters/oracle_enhanced_schema_dumper' # Implementation of structure dump require 'active_record/connection_adapters/oracle_enhanced_structure_dump' # Add BigDecimal#to_d, Fixnum#to_d and Bignum#to_d methods if not already present require 'active_record/connection_adapters/oracle_enhanced_core_ext' require 'active_record/connection_adapters/oracle_enhanced_activerecord_patches' require 'active_record/connection_adapters/oracle_enhanced_version'