# -*- 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'