ArJdbc.load_java_part :Oracle module ArJdbc module Oracle require 'arjdbc/oracle/column' # @private def self.extended(adapter); initialize!; end # @private @@_initialized = nil # @private def self.initialize! return if @@_initialized; @@_initialized = true require 'arjdbc/util/serialized_attributes' Util::SerializedAttributes.setup /LOB\(|LOB$/i, 'after_save_with_oracle_lob' unless ActiveRecord::ConnectionAdapters::AbstractAdapter. instance_methods(false).detect { |m| m.to_s == "prefetch_primary_key?" } require 'arjdbc/jdbc/quoted_primary_key' ActiveRecord::Base.extend ArJdbc::QuotedPrimaryKeyExtension end end # @see ActiveRecord::ConnectionAdapters::JdbcAdapter#jdbc_connection_class def self.jdbc_connection_class ::ActiveRecord::ConnectionAdapters::OracleJdbcConnection end # @see ActiveRecord::ConnectionAdapters::JdbcAdapter#jdbc_column_class def jdbc_column_class; ::ActiveRecord::ConnectionAdapters::OracleColumn end # @private @@update_lob_values = true # Updating records with LOB values (binary/text columns) in a separate # statement can be disabled using : # # ArJdbc::Oracle.update_lob_values = false # # @note This only applies when prepared statements are not used. def self.update_lob_values?; @@update_lob_values; end # @see #update_lob_values? def self.update_lob_values=(update); @@update_lob_values = update; end # @see #update_lob_values? # @see ArJdbc::Util::SerializedAttributes#update_lob_columns def update_lob_value?(value, column = nil) Oracle.update_lob_values? && ! prepared_statements? && ! ( value.nil? || value == '' ) end # @private @@emulate_booleans = true # Boolean emulation can be disabled using : # # ArJdbc::Oracle.emulate_booleans = false # # @see ActiveRecord::ConnectionAdapters::OracleAdapter#emulate_booleans def self.emulate_booleans?; @@emulate_booleans; end # @deprecated Use {#emulate_booleans?} instead. def self.emulate_booleans; @@emulate_booleans; end # @see #emulate_booleans? def self.emulate_booleans=(emulate); @@emulate_booleans = emulate; end class TableDefinition < ::ActiveRecord::ConnectionAdapters::TableDefinition def raw(*args) options = args.extract_options! column(args[0], 'raw', options) end def xml(*args) options = args.extract_options! column(args[0], 'xml', options) end end def table_definition(*args) new_table_definition(TableDefinition, *args) end def self.arel_visitor_type(config = nil) ::Arel::Visitors::Oracle end # @see ActiveRecord::ConnectionAdapters::JdbcAdapter#bind_substitution # @private class BindSubstitution < ::Arel::Visitors::Oracle include ::Arel::Visitors::BindVisitor end if defined? ::Arel::Visitors::BindVisitor ADAPTER_NAME = 'Oracle'.freeze def adapter_name ADAPTER_NAME end 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" }, :timestamp => { :name => "TIMESTAMP" }, :time => { :name => "DATE" }, :date => { :name => "DATE" }, :binary => { :name => "BLOB" }, :boolean => { :name => "NUMBER", :limit => 1 }, :raw => { :name => "RAW", :limit => 2000 }, :xml => { :name => 'XMLTYPE' } } def native_database_types super.merge(NATIVE_DATABASE_TYPES) end def modify_types(types) super(types) NATIVE_DATABASE_TYPES.each do |key, value| types[key] = value.dup end types end # Prevent ORA-01795 for in clauses with more than 1000 def in_clause_length 1000 end alias_method :ids_in_list_limit, :in_clause_length IDENTIFIER_LENGTH = 30 # maximum length of Oracle identifiers is 30 def table_alias_length; IDENTIFIER_LENGTH; end def table_name_length; IDENTIFIER_LENGTH; end def index_name_length; IDENTIFIER_LENGTH; end def column_name_length; IDENTIFIER_LENGTH; end def default_sequence_name(table_name, column = nil) # TODO: remove schema prefix if present (before truncating) "#{table_name.to_s[0, IDENTIFIER_LENGTH - 4]}_seq" end # @override def create_table(name, options = {}) super(name, options) unless options[:id] == false seq_name = options[:sequence_name] || default_sequence_name(name) start_value = options[:sequence_start_value] || 10000 raise ActiveRecord::StatementInvalid.new("name #{seq_name} too long") if seq_name.length > table_alias_length execute "CREATE SEQUENCE #{quote_table_name(seq_name)} START WITH #{start_value}" end end # @override def rename_table(name, new_name) if new_name.to_s.length > table_name_length raise ArgumentError, "New table name '#{new_name}' is too long; the limit is #{table_name_length} characters" end if "#{new_name}_seq".to_s.length > sequence_name_length raise ArgumentError, "New sequence name '#{new_name}_seq' is too long; the limit is #{sequence_name_length} characters" end execute "RENAME #{quote_table_name(name)} TO #{quote_table_name(new_name)}" execute "RENAME #{quote_table_name("#{name}_seq")} TO #{quote_table_name("#{new_name}_seq")}" rescue nil end # @override def drop_table(name, options = {}) outcome = super(name) return outcome if name == 'schema_migrations' seq_name = options.key?(:sequence_name) ? # pass nil/false - no sequence options[:sequence_name] : default_sequence_name(name) return outcome unless seq_name execute "DROP SEQUENCE #{quote_table_name(seq_name)}" rescue nil end # @override def type_to_sql(type, limit = nil, precision = nil, scale = nil) case type.to_sym when :binary # { BLOB | BINARY LARGE OBJECT } [ ( length [{K |M |G }] ) ] # although Oracle does not like limit (length) with BLOB (or CLOB) : # # CREATE TABLE binaries (data BLOB, short_data BLOB(1024)); # ORA-00907: missing right parenthesis * # # TODO do we need to worry about NORMAL vs. non IN-TABLE BLOBs ?! # http://dba.stackexchange.com/questions/8770/improve-blob-writing-performance-in-oracle-11g # - if the LOB is smaller than 3900 bytes it can be stored inside the # table row; by default this is enabled, # unless you specify DISABLE STORAGE IN ROW # - normal LOB - stored in a separate segment, outside of table, # you may even put it in another tablespace; super(type, nil, nil, nil) when :text super(type, nil, nil, nil) else super end end def indexes(table, name = nil) @connection.indexes(table, name, @connection.connection.meta_data.user_name) end # @note Only used with (non-AREL) ActiveRecord **2.3**. # @see Arel::Visitors::Oracle def add_limit_offset!(sql, options) offset = options[:offset] || 0 if limit = options[:limit] sql.replace "SELECT * FROM " << "(select raw_sql_.*, rownum raw_rnum_ from (#{sql}) raw_sql_ where rownum <= #{offset + limit})" << " WHERE raw_rnum_ > #{offset}" elsif offset > 0 sql.replace "SELECT * FROM " << "(select raw_sql_.*, rownum raw_rnum_ from (#{sql}) raw_sql_)" << " WHERE raw_rnum_ > #{offset}" end end if ::ActiveRecord::VERSION::MAJOR < 3 def current_user @current_user ||= execute("SELECT sys_context('userenv', 'session_user') su FROM dual").first['su'] end def current_database @current_database ||= execute("SELECT sys_context('userenv', 'db_name') db FROM dual").first['db'] end def current_schema execute("SELECT sys_context('userenv', 'current_schema') schema FROM dual").first['schema'] end def current_schema=(schema_owner) execute("ALTER SESSION SET current_schema=#{schema_owner}") end # @override def release_savepoint(name = nil) # no RELEASE SAVEPOINT statement in Oracle (JDBC driver throws "Unsupported feature") end def remove_index(table_name, options = {}) execute "DROP INDEX #{index_name(table_name, options)}" end def change_column_default(table_name, column_name, default) execute "ALTER TABLE #{quote_table_name(table_name)} " + "MODIFY #{quote_column_name(column_name)} DEFAULT #{quote(default)}" end # @override def add_column_options!(sql, options) # handle case of defaults for CLOB columns, which would otherwise get "quoted" incorrectly if options_include_default?(options) && (column = options[:column]) && column.type == :text sql << " DEFAULT #{quote(options.delete(:default))}" end super end # @override def change_column(table_name, column_name, type, options = {}) change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} " << "MODIFY #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit])}" add_column_options!(change_column_sql, options) execute(change_column_sql) end # @override def rename_column(table_name, column_name, new_column_name) execute "ALTER TABLE #{quote_table_name(table_name)} " << "RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}" end if ActiveRecord::VERSION::MAJOR >= 4 # @override def remove_column(table_name, column_name, type = nil, options = {}) do_remove_column(table_name, column_name) end else # @override def remove_column(table_name, *column_names) for column_name in column_names.flatten do_remove_column(table_name, column_name) end end alias remove_columns remove_column end def do_remove_column(table_name, column_name) execute "ALTER TABLE #{quote_table_name(table_name)} DROP COLUMN #{quote_column_name(column_name)}" end private :do_remove_column # 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") # # @override def distinct(columns, order_by) "DISTINCT #{columns_for_distinct(columns, order_by)}" end # @override Since AR 4.0 (on 4.1 {#distinct} is gone and won't be called). def columns_for_distinct(columns, orders) return columns if orders.blank? if orders.is_a?(Array) # AR 3.x vs 4.x orders = orders.map { |column| column.is_a?(String) ? column : column.to_sql } else orders = extract_order_columns(orders) end # 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 = orders.map do |c, i| "FIRST_VALUE(#{c.split.first}) OVER (PARTITION BY #{columns} ORDER BY #{c}) AS alias_#{i}__" end columns = [ columns ]; columns.flatten! columns.push( *order_columns ).join(', ') end # ORDER BY clause for the passed order option. # # Uses column aliases as defined by {#distinct}. def add_order_by_for_association_limiting!(sql, options) return sql if options[:order].blank? order_columns = extract_order_columns(options[:order]) do |columns| columns.map! { |s| $1 if s =~ / (.*)/ }; columns end order = order_columns.map { |s, i| "alias_#{i}__ #{s}" } # @see {#distinct} sql << "ORDER BY #{order.join(', ')}" end def extract_order_columns(order_by) columns = order_by.split(',') columns.map!(&:strip); columns.reject!(&:blank?) columns = yield(columns) if block_given? columns.zip( (0...columns.size).to_a ) end private :extract_order_columns def temporary_table?(table_name) select_value("SELECT temporary FROM user_tables WHERE table_name = '#{table_name.upcase}'") == 'Y' end def tables @connection.tables(nil, oracle_schema) end # NOTE: better to use current_schema instead of the configured one ?! def columns(table_name, name = nil) @connection.columns_internal(table_name.to_s, nil, oracle_schema) end def tablespace(table_name) select_value "SELECT tablespace_name FROM user_tables WHERE table_name='#{table_name.to_s.upcase}'" end def charset database_parameters['NLS_CHARACTERSET'] end def collation database_parameters['NLS_COMP'] end def database_parameters return @database_parameters unless ( @database_parameters ||= {} ).empty? @connection.execute_query_raw("SELECT * FROM NLS_DATABASE_PARAMETERS") do |name, value| @database_parameters[name] = value end @database_parameters end # QUOTING ================================================== # @override def quote_table_name(name) name.to_s.split('.').map{ |n| n.split('@').map{ |m| quote_column_name(m) }.join('@') }.join('.') end # @override def quote_column_name(name) # if only valid lowercase column characters in name if ( name = name.to_s ) =~ /\A[a-z][a-z_0-9\$#]*\Z/ # putting double-quotes around an identifier causes Oracle to treat the # identifier as case sensitive (otherwise assumes case-insensitivity) ! # all upper case is an exception, where double-quotes are meaningless "\"#{name.upcase}\"" # name.upcase else # remove double quotes which cannot be used inside quoted identifier "\"#{name.gsub('"', '')}\"" end end def unquote_table_name(name) name = name[1...-1] if name[0, 1] == '"' name.upcase == name ? name.downcase : name end # @override def quote(value, column = nil) return value if sql_literal?(value) column_type = column && column.type if column_type == :text || column_type == :binary return 'NULL' if value.nil? || value == '' if update_lob_value?(value, column) if /(.*?)\([0-9]+\)/ =~ ( sql_type = column.sql_type ) %Q{empty_#{ $1.downcase }()} else %Q{empty_#{ sql_type.respond_to?(:downcase) ? sql_type.downcase : 'blob' }()} end else "'#{quote_string(value.to_s)}'" end elsif column_type == :xml "XMLTYPE('#{quote_string(value)}')" # XMLTYPE ? elsif column_type == :raw quote_raw(value) else if column.respond_to?(:primary) && column.primary && column.klass != String return value.to_i.to_s end if column_type == :datetime || column_type == :time if value.acts_like?(:time) %Q{TO_DATE('#{get_time(value).strftime("%Y-%m-%d %H:%M:%S")}','YYYY-MM-DD HH24:MI:SS')} else value.blank? ? 'NULL' : %Q{DATE'#{value}'} # assume correctly formated DATE (string) end elsif ( like_date = value.acts_like?(:date) ) || column_type == :date if value.acts_like?(:time) # value.respond_to?(:strftime) %Q{DATE'#{get_time(value).strftime("%Y-%m-%d")}'} elsif like_date %Q{DATE'#{quoted_date(value)}'} # DATE 'YYYY-MM-DD' else value.blank? ? 'NULL' : %Q{DATE'#{value}'} # assume correctly formated DATE (string) end elsif ( like_time = value.acts_like?(:time) ) || column_type == :timestamp if like_time %Q{TIMESTAMP'#{quoted_date(value, true)}'} # TIMESTAMP 'YYYY-MM-DD HH24:MI:SS.FF' else value.blank? ? 'NULL' : %Q{TIMESTAMP'#{value}'} # assume correctly formated TIMESTAMP (string) end else super end end end # Quote date/time values for use in SQL input. # Includes milliseconds if the value is a Time responding to usec. # @override def quoted_date(value, time = nil) if time || ( time.nil? && value.acts_like?(:time) ) usec = value.respond_to?(:usec) && (value.usec / 10000.0).round # .428000 -> .43 return "#{get_time(value).to_s(:db)}.#{sprintf("%02d", usec)}" if usec # value.strftime("%Y-%m-%d %H:%M:%S") end value.to_s(:db) end def quote_raw(value) value = value.unpack('C*') if value.is_a?(String) "'#{value.map { |x| "%02X" % x }.join}'" end # @override def supports_migrations?; true end # @override def supports_primary_key?; true end # @override def supports_savepoints?; true end # @override def supports_explain?; true end # @override def supports_views?; true end def explain(arel, binds = []) sql = "EXPLAIN PLAN FOR #{to_sql(arel, binds)}" return if sql =~ /FROM all_/ exec_update(sql, 'EXPLAIN', binds) select_values("SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY)", 'EXPLAIN').join("\n") end def select(sql, name = nil, binds = []) result = super # AR::Result (4.0) or Array (<= 3.2) result.columns.delete('raw_rnum_') if result.respond_to?(:columns) result.each { |row| row.delete('raw_rnum_') } # Hash rows even for AR::Result result end # Returns true for Oracle adapter (since Oracle requires primary key # values to be pre-fetched before insert). # @see #next_sequence_value # @override def prefetch_primary_key?(table_name = nil) return true if table_name.nil? table_name = table_name.to_s columns(table_name).count { |column| column.primary } == 1 end # @override def next_sequence_value(sequence_name) sequence_name = quote_table_name(sequence_name) sql = "SELECT #{sequence_name}.NEXTVAL id FROM dual" log(sql, 'SQL') { @connection.next_sequence_value(sequence_name) } end # @override (for AR <= 3.0) def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) # if PK is already pre-fetched from sequence or if there is no PK : if id_value || pk.nil? execute(sql, name) return id_value end if pk && use_insert_returning? # true by default on AR <= 3.0 sql = "#{sql} RETURNING #{quote_column_name(pk)} INTO ?" exec_insert_returning(sql, name, nil, pk) else execute(sql, name) end end protected :insert_sql # @override def sql_for_insert(sql, pk, id_value, sequence_name, binds) unless id_value || pk.nil? if pk && use_insert_returning? sql = "#{sql} RETURNING #{quote_column_name(pk)} INTO ?" end end [ sql, binds ] end # @override def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = []) # NOTE: ActiveRecord::Relation calls our {#next_sequence_value} # (from its `insert`) and passes the returned id_value here ... sql, binds = sql_for_insert(to_sql(arel, binds), pk, id_value, sequence_name, binds) if id_value exec_update(sql, name, binds) return id_value else value = exec_insert(sql, name, binds, pk, sequence_name) id_value || last_inserted_id(value) end end # @override def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) if pk && use_insert_returning? if sql.is_a?(String) && sql.index('RETURNING') return exec_insert_returning(sql, name, binds, pk) end end super(sql, name, binds) # assume no generated id for table end def exec_insert_returning(sql, name, binds, pk = nil) sql = to_sql(sql, binds) if sql.respond_to?(:to_sql) if prepared_statements? log(sql, name, binds) { @connection.execute_insert_returning(sql, binds) } else log(sql, name) { @connection.execute_insert_returning(sql, nil) } end end # private :exec_insert_returning def next_id_value(sql, sequence_name = nil) # Assume the SQL contains a bind-variable for the ID sequence_name ||= begin # Extract the table from the insert SQL. Yuck. table = extract_table_ref_from_insert_sql(sql) default_sequence_name(table) end next_sequence_value(sequence_name) end private :next_id_value def use_insert_returning? if @use_insert_returning.nil? @use_insert_returning = false end @use_insert_returning end private def _execute(sql, name = nil) if self.class.select?(sql) @connection.execute_query_raw(sql) elsif self.class.insert?(sql) @connection.execute_insert(sql) else @connection.execute_update(sql) end end def extract_table_ref_from_insert_sql(sql) table = sql.split(" ", 4)[2] if idx = table.index('(') table = table[0...idx] # INTO table(col1, col2) ... end unquote_table_name(table) end # In Oracle, schemas are usually created under your username : # http://www.oracle.com/technology/obe/2day_dba/schema/schema.htm # # A schema is the set of objects (tables, views, indexes, etc) that belongs # to an user, often used as another way to refer to an Oracle user account. # # But allow separate configuration as "schema:" anyway (see #53) def oracle_schema if @config[:schema] @config[:schema].to_s elsif @config[:username] @config[:username].to_s end end end end require 'arjdbc/util/quoted_cache' module ActiveRecord::ConnectionAdapters remove_const(:OracleAdapter) if const_defined?(:OracleAdapter) class OracleAdapter < JdbcAdapter include ::ArJdbc::Oracle include ::ArJdbc::Util::QuotedCache # By default, the MysqlAdapter will consider all columns of type # tinyint(1) as boolean. If you wish to disable this : # # ActiveRecord::ConnectionAdapters::OracleAdapter.emulate_booleans = false # def self.emulate_booleans?; ::ArJdbc::Oracle.emulate_booleans?; end def self.emulate_booleans; ::ArJdbc::Oracle.emulate_booleans?; end # oracle-enhanced def self.emulate_booleans=(emulate); ::ArJdbc::Oracle.emulate_booleans = emulate; end def initialize(*args) ::ArJdbc::Oracle.initialize! super # configure_connection happens in super @use_insert_returning = config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(config[:insert_returning]) : nil end end class OracleColumn < JdbcColumn include ::ArJdbc::Oracle::Column # def returning_id?; @returning_id ||= nil end # def returning_id!; @returning_id = true end end end