pmacs-activerecord-oracle_enhanced-adapter 1.4.2.rc1
Sign up to get free protection for your applications and to get access to all the features.
- data/.rspec +2 -0
- data/Gemfile +52 -0
- data/History.md +284 -0
- data/License.txt +20 -0
- data/README.md +403 -0
- data/RUNNING_TESTS.md +45 -0
- data/Rakefile +59 -0
- data/VERSION +1 -0
- data/lib/active_record/connection_adapters/emulation/oracle_adapter.rb +5 -0
- data/lib/active_record/connection_adapters/oracle_enhanced.rake +105 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_activerecord_patches.rb +41 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +1408 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_base_ext.rb +118 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_column.rb +141 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_connection.rb +135 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_context_index.rb +359 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_core_ext.rb +25 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_cpk.rb +21 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb +44 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb +565 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb +491 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_procedures.rb +260 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_schema_definitions.rb +231 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_schema_dumper.rb +257 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements.rb +397 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements_ext.rb +265 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_structure_dump.rb +294 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_tasks.rb +17 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_version.rb +1 -0
- data/lib/pmacs-activerecord-oracle_enhanced-adapter.rb +25 -0
- data/pmacs-activerecord-oracle_enhanced-adapter.gemspec +131 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +778 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb +332 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_context_index_spec.rb +427 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_core_ext_spec.rb +19 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb +113 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +1376 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_dbms_output_spec.rb +69 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb +141 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_emulate_oracle_adapter_spec.rb +25 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb +378 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb +438 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_schema_statements_spec.rb +1280 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_structure_dump_spec.rb +339 -0
- data/spec/spec_helper.rb +187 -0
- metadata +302 -0
data/RUNNING_TESTS.md
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
Creating the test database
|
2
|
+
--------------------------
|
3
|
+
|
4
|
+
You need Oracle database (version 10.2 or later) with SYS and SYSTEM user access.
|
5
|
+
|
6
|
+
If you are on a Mac OS X 10.6 then use [these instructions](http://blog.rayapps.com/2009/09/14/how-to-install-oracle-database-10g-on-mac-os-x-snow-leopard) to install local Oracle DB 10.2.0.4. Other option is to use Linux VM and install Oracle DB on it.
|
7
|
+
|
8
|
+
If you are on Linux (or will use Linux virtual machine) and need Oracle DB just for running tests then Oracle DB XE edition is enough. See [Oracle XE downloads page](http://www.oracle.com/technetwork/database/express-edition/downloads/index.html) for download links and instructions.
|
9
|
+
|
10
|
+
If you are getting ORA-12520 errors when running tests then it means that Oracle cannot create enough processes to handle many connections (as during tests many connections are created and destroyed). In this case you need to log in as SYSTEM user and execute e.g.
|
11
|
+
|
12
|
+
alter system set processes=200 scope=spfile;
|
13
|
+
|
14
|
+
to increase process limit and then restart the database (this will be necessary if Oracle XE will be used as default processes limit is 40).
|
15
|
+
|
16
|
+
Ruby versions
|
17
|
+
-------------
|
18
|
+
|
19
|
+
It is recommended to use [RVM](http://rvm.beginrescueend.com) to run tests with different Ruby implementations. oracle_enhanced is mainly tested with MRI 1.8.7 (all Rails versions) and 1.9.2 (Rails 3) and JRuby 1.6.
|
20
|
+
|
21
|
+
Running tests
|
22
|
+
-------------
|
23
|
+
|
24
|
+
* Create Oracle database schema for test purposes. Review `spec/spec_helper.rb` to see default schema/user names and database names (use environment variables to override defaults)
|
25
|
+
|
26
|
+
SQL> CREATE USER oracle_enhanced IDENTIFIED BY oracle_enhanced;
|
27
|
+
SQL> GRANT unlimited tablespace, create session, create table, create sequence, create procedure, create trigger, create view, create materialized view, create database link, create synonym, create type, ctxapp TO oracle_enhanced;
|
28
|
+
|
29
|
+
* If you use RVM then switch to corresponding Ruby (1.8.7, 1.9.2 or JRuby) and it is recommended to create isolated gemset for test purposes (e.g. rvm create gemset oracle_enhanced)
|
30
|
+
|
31
|
+
* Install bundler with
|
32
|
+
|
33
|
+
gem install bundler
|
34
|
+
|
35
|
+
* Set RAILS_GEM_VERSION to Rails version that you would like to use in oracle_enhanced tests, e.g.
|
36
|
+
|
37
|
+
export RAILS_GEM_VERSION=3.0.3
|
38
|
+
|
39
|
+
* Install necessary gems with
|
40
|
+
|
41
|
+
bundle install
|
42
|
+
|
43
|
+
* Run tests with
|
44
|
+
|
45
|
+
rake spec
|
data/Rakefile
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'rake'
|
12
|
+
|
13
|
+
require 'jeweler'
|
14
|
+
Jeweler::Tasks.new do |gem|
|
15
|
+
gem.name = "pmacs-activerecord-oracle_enhanced-adapter"
|
16
|
+
gem.summary = "Oracle enhanced adapter for ActiveRecord"
|
17
|
+
gem.description = <<-EOS
|
18
|
+
Oracle "enhanced" ActiveRecord adapter contains useful additional methods for working with new and legacy Oracle databases.
|
19
|
+
This adapter is superset of original ActiveRecord Oracle adapter.
|
20
|
+
EOS
|
21
|
+
gem.email = "charles.treatman@gmail.com"
|
22
|
+
gem.homepage = "http://github.com/pmacs/oracle-enhanced"
|
23
|
+
gem.authors = ["Charles Treatman", "Raimonds Simanovskis"]
|
24
|
+
gem.extra_rdoc_files = ['README.md']
|
25
|
+
end
|
26
|
+
Jeweler::RubygemsDotOrgTasks.new
|
27
|
+
|
28
|
+
require 'rspec/core/rake_task'
|
29
|
+
RSpec::Core::RakeTask.new(:spec)
|
30
|
+
|
31
|
+
RSpec::Core::RakeTask.new(:rcov) do |t|
|
32
|
+
t.rcov = true
|
33
|
+
t.rcov_opts = ['--exclude', '/Library,spec/']
|
34
|
+
end
|
35
|
+
|
36
|
+
desc "Clear test database"
|
37
|
+
task :clear do
|
38
|
+
require "./spec/spec_helper"
|
39
|
+
ActiveRecord::Base.establish_connection(CONNECTION_PARAMS)
|
40
|
+
require "active_support/core_ext"
|
41
|
+
ActiveRecord::Base.connection.execute_structure_dump(ActiveRecord::Base.connection.full_drop)
|
42
|
+
ActiveRecord::Base.connection.execute("PURGE RECYCLEBIN") rescue nil
|
43
|
+
end
|
44
|
+
|
45
|
+
# Clear test database before running spec and rcov
|
46
|
+
task :spec => :clear
|
47
|
+
task :rcov => :clear
|
48
|
+
|
49
|
+
task :default => :spec
|
50
|
+
|
51
|
+
require 'rdoc/task'
|
52
|
+
Rake::RDocTask.new do |rdoc|
|
53
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
54
|
+
|
55
|
+
rdoc.rdoc_dir = 'doc'
|
56
|
+
rdoc.title = "activerecord-oracle_enhanced-adapter #{version}"
|
57
|
+
rdoc.rdoc_files.include('README*')
|
58
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
59
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.4.2.rc1
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# implementation idea taken from JDBC adapter
|
2
|
+
# added possibility to execute previously defined task (passed as argument to task block)
|
3
|
+
def redefine_task(*args, &block)
|
4
|
+
task_name = Hash === args.first ? args.first.keys[0] : args.first
|
5
|
+
existing_task = Rake.application.lookup task_name
|
6
|
+
existing_actions = nil
|
7
|
+
if existing_task
|
8
|
+
class << existing_task; public :instance_variable_set, :instance_variable_get; end
|
9
|
+
existing_task.instance_variable_set "@prerequisites", FileList[]
|
10
|
+
existing_actions = existing_task.instance_variable_get "@actions"
|
11
|
+
existing_task.instance_variable_set "@actions", []
|
12
|
+
end
|
13
|
+
task(*args) do
|
14
|
+
block.call(existing_actions)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Creates database user with db:create
|
19
|
+
if defined?(create_database) == 'method'
|
20
|
+
def create_database_with_oracle_enhanced(config)
|
21
|
+
if config['adapter'] == 'oracle_enhanced'
|
22
|
+
print "Please provide the SYSTEM password for your oracle installation\n>"
|
23
|
+
system_password = $stdin.gets.strip
|
24
|
+
ActiveRecord::Base.establish_connection(config.merge('username' => 'SYSTEM', 'password' => system_password))
|
25
|
+
begin
|
26
|
+
ActiveRecord::Base.connection.execute "CREATE USER #{config['username']} IDENTIFIED BY #{config['password']}"
|
27
|
+
rescue => e
|
28
|
+
if e.message =~ /ORA-01920/ # user name conflicts with another user or role name
|
29
|
+
ActiveRecord::Base.connection.execute "ALTER USER #{config['username']} IDENTIFIED BY #{config['password']}"
|
30
|
+
else
|
31
|
+
raise e
|
32
|
+
end
|
33
|
+
end
|
34
|
+
ActiveRecord::Base.connection.execute "GRANT unlimited tablespace TO #{config['username']}"
|
35
|
+
ActiveRecord::Base.connection.execute "GRANT create session TO #{config['username']}"
|
36
|
+
ActiveRecord::Base.connection.execute "GRANT create table TO #{config['username']}"
|
37
|
+
ActiveRecord::Base.connection.execute "GRANT create sequence TO #{config['username']}"
|
38
|
+
else
|
39
|
+
create_database_without_oracle_enhanced(config)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
alias :create_database_without_oracle_enhanced :create_database
|
43
|
+
alias :create_database :create_database_with_oracle_enhanced
|
44
|
+
end
|
45
|
+
|
46
|
+
# Drops database user with db:drop
|
47
|
+
if defined?(drop_database) == 'method'
|
48
|
+
def drop_database_with_oracle_enhanced(config)
|
49
|
+
if config['adapter'] == 'oracle_enhanced'
|
50
|
+
ActiveRecord::Base.establish_connection(config)
|
51
|
+
ActiveRecord::Base.connection.execute_structure_dump(ActiveRecord::Base.connection.full_drop)
|
52
|
+
else
|
53
|
+
drop_database_without_oracle_enhanced(config)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
alias :drop_database_without_oracle_enhanced :drop_database
|
57
|
+
alias :drop_database :drop_database_with_oracle_enhanced
|
58
|
+
end
|
59
|
+
|
60
|
+
namespace :db do
|
61
|
+
|
62
|
+
namespace :structure do
|
63
|
+
redefine_task :dump => :environment do |existing_actions|
|
64
|
+
abcs = ActiveRecord::Base.configurations
|
65
|
+
rails_env = defined?(Rails.env) ? Rails.env : RAILS_ENV
|
66
|
+
if abcs[rails_env]['adapter'] == 'oracle_enhanced'
|
67
|
+
ActiveRecord::Base.establish_connection(abcs[rails_env])
|
68
|
+
File.open("db/#{rails_env}_structure.sql", "w+") { |f| f << ActiveRecord::Base.connection.structure_dump }
|
69
|
+
if ActiveRecord::Base.connection.supports_migrations?
|
70
|
+
File.open("db/#{rails_env}_structure.sql", "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information }
|
71
|
+
end
|
72
|
+
if abcs[rails_env]['structure_dump'] == "db_stored_code"
|
73
|
+
File.open("db/#{rails_env}_structure.sql", "a") { |f| f << ActiveRecord::Base.connection.structure_dump_db_stored_code }
|
74
|
+
end
|
75
|
+
else
|
76
|
+
Array(existing_actions).each{|action| action.call}
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
namespace :test do
|
82
|
+
redefine_task :clone_structure => [ "db:structure:dump", "db:test:purge" ] do |existing_actions|
|
83
|
+
abcs = ActiveRecord::Base.configurations
|
84
|
+
rails_env = defined?(Rails.env) ? Rails.env : RAILS_ENV
|
85
|
+
if abcs[rails_env]['adapter'] == 'oracle_enhanced' && abcs['test']['adapter'] == 'oracle_enhanced'
|
86
|
+
ActiveRecord::Base.establish_connection(:test)
|
87
|
+
ActiveRecord::Base.connection.execute_structure_dump(File.read("db/#{rails_env}_structure.sql"))
|
88
|
+
else
|
89
|
+
Array(existing_actions).each{|action| action.call}
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
redefine_task :purge => :environment do |existing_actions|
|
94
|
+
abcs = ActiveRecord::Base.configurations
|
95
|
+
if abcs['test']['adapter'] == 'oracle_enhanced'
|
96
|
+
ActiveRecord::Base.establish_connection(:test)
|
97
|
+
ActiveRecord::Base.connection.execute_structure_dump(ActiveRecord::Base.connection.full_drop)
|
98
|
+
ActiveRecord::Base.connection.execute("PURGE RECYCLEBIN") rescue nil
|
99
|
+
else
|
100
|
+
Array(existing_actions).each{|action| action.call}
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# ActiveRecord 2.3 patches
|
2
|
+
if ActiveRecord::VERSION::MAJOR == 2 && ActiveRecord::VERSION::MINOR == 3
|
3
|
+
require "active_record/associations"
|
4
|
+
|
5
|
+
ActiveRecord::Associations::ClassMethods.module_eval do
|
6
|
+
private
|
7
|
+
def tables_in_string(string)
|
8
|
+
return [] if string.blank?
|
9
|
+
if self.connection.adapter_name == "OracleEnhanced"
|
10
|
+
# always convert table names to downcase as in Oracle quoted table names are in uppercase
|
11
|
+
# ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries
|
12
|
+
string.scan(/([a-zA-Z_][\.\w]+).?\./).flatten.map(&:downcase).uniq - ['raw_sql_']
|
13
|
+
else
|
14
|
+
string.scan(/([\.a-zA-Z_]+).?\./).flatten
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation.class_eval do
|
20
|
+
protected
|
21
|
+
def aliased_table_name_for(name, suffix = nil)
|
22
|
+
# always downcase quoted table name as Oracle quoted table names are in uppercase
|
23
|
+
if !parent.table_joins.blank? && parent.table_joins.to_s.downcase =~ %r{join(\s+\w+)?\s+#{active_record.connection.quote_table_name(name).downcase}\son}
|
24
|
+
@join_dependency.table_aliases[name] += 1
|
25
|
+
end
|
26
|
+
|
27
|
+
unless @join_dependency.table_aliases[name].zero?
|
28
|
+
# if the table name has been used, then use an alias
|
29
|
+
name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}"
|
30
|
+
table_index = @join_dependency.table_aliases[name]
|
31
|
+
@join_dependency.table_aliases[name] += 1
|
32
|
+
name = name[0..active_record.connection.table_alias_length-3] + "_#{table_index+1}" if table_index > 0
|
33
|
+
else
|
34
|
+
@join_dependency.table_aliases[name] += 1
|
35
|
+
end
|
36
|
+
|
37
|
+
name
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,1408 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
# oracle_enhanced_adapter.rb -- ActiveRecord adapter for Oracle 8i, 9i, 10g, 11g
|
3
|
+
#
|
4
|
+
# Authors or original oracle_adapter: Graham Jenkins, Michael Schoen
|
5
|
+
#
|
6
|
+
# Current maintainer: Raimonds Simanovskis (http://blog.rayapps.com)
|
7
|
+
#
|
8
|
+
#########################################################################
|
9
|
+
#
|
10
|
+
# See History.md for changes added to original oracle_adapter.rb
|
11
|
+
#
|
12
|
+
#########################################################################
|
13
|
+
#
|
14
|
+
# From original oracle_adapter.rb:
|
15
|
+
#
|
16
|
+
# Implementation notes:
|
17
|
+
# 1. Redefines (safely) a method in ActiveRecord to make it possible to
|
18
|
+
# implement an autonumbering solution for Oracle.
|
19
|
+
# 2. The OCI8 driver is patched to properly handle values for LONG and
|
20
|
+
# TIMESTAMP columns. The driver-author has indicated that a future
|
21
|
+
# release of the driver will obviate this patch.
|
22
|
+
# 3. LOB support is implemented through an after_save callback.
|
23
|
+
# 4. Oracle does not offer native LIMIT and OFFSET options; this
|
24
|
+
# functionality is mimiced through the use of nested selects.
|
25
|
+
# See http://asktom.oracle.com/pls/ask/f?p=4950:8:::::F4950_P8_DISPLAYID:127412348064
|
26
|
+
#
|
27
|
+
# Do what you want with this code, at your own peril, but if any
|
28
|
+
# significant portion of my code remains then please acknowledge my
|
29
|
+
# contribution.
|
30
|
+
# portions Copyright 2005 Graham Jenkins
|
31
|
+
|
32
|
+
# ActiveRecord 2.2 does not load version file automatically
|
33
|
+
require 'active_record/version' unless defined?(ActiveRecord::VERSION)
|
34
|
+
|
35
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
36
|
+
require 'active_record/connection_adapters/oracle_enhanced_connection'
|
37
|
+
|
38
|
+
require 'active_record/connection_adapters/oracle_enhanced_base_ext'
|
39
|
+
require 'active_record/connection_adapters/oracle_enhanced_column'
|
40
|
+
|
41
|
+
require 'digest/sha1'
|
42
|
+
|
43
|
+
module ActiveRecord
|
44
|
+
module ConnectionAdapters #:nodoc:
|
45
|
+
|
46
|
+
# Oracle enhanced adapter will work with both
|
47
|
+
# Ruby 1.8/1.9 ruby-oci8 gem (which provides interface to Oracle OCI client)
|
48
|
+
# or with JRuby and Oracle JDBC driver.
|
49
|
+
#
|
50
|
+
# It should work with Oracle 9i, 10g and 11g databases.
|
51
|
+
# Limited set of functionality should work on Oracle 8i as well but several features
|
52
|
+
# rely on newer functionality in Oracle database.
|
53
|
+
#
|
54
|
+
# Usage notes:
|
55
|
+
# * Key generation assumes a "${table_name}_seq" sequence is available
|
56
|
+
# for all tables; the sequence name can be changed using
|
57
|
+
# ActiveRecord::Base.set_sequence_name. When using Migrations, these
|
58
|
+
# sequences are created automatically.
|
59
|
+
# Use set_sequence_name :autogenerated with legacy tables that have
|
60
|
+
# triggers that populate primary keys automatically.
|
61
|
+
# * Oracle uses DATE or TIMESTAMP datatypes for both dates and times.
|
62
|
+
# Consequently some hacks are employed to map data back to Date or Time
|
63
|
+
# in Ruby. Timezones and sub-second precision on timestamps are
|
64
|
+
# not supported.
|
65
|
+
# * Default values that are functions (such as "SYSDATE") are not
|
66
|
+
# supported. This is a restriction of the way ActiveRecord supports
|
67
|
+
# default values.
|
68
|
+
#
|
69
|
+
# Required parameters:
|
70
|
+
#
|
71
|
+
# * <tt>:username</tt>
|
72
|
+
# * <tt>:password</tt>
|
73
|
+
# * <tt>:database</tt> - either TNS alias or connection string for OCI client or database name in JDBC connection string
|
74
|
+
#
|
75
|
+
# Optional parameters:
|
76
|
+
#
|
77
|
+
# * <tt>:host</tt> - host name for JDBC connection, defaults to "localhost"
|
78
|
+
# * <tt>:port</tt> - port number for JDBC connection, defaults to 1521
|
79
|
+
# * <tt>:privilege</tt> - set "SYSDBA" if you want to connect with this privilege
|
80
|
+
# * <tt>:allow_concurrency</tt> - set to "true" if non-blocking mode should be enabled (just for OCI client)
|
81
|
+
# * <tt>:prefetch_rows</tt> - how many rows should be fetched at one time to increase performance, defaults to 100
|
82
|
+
# * <tt>:cursor_sharing</tt> - cursor sharing mode to minimize amount of unique statements, defaults to "force"
|
83
|
+
# * <tt>:time_zone</tt> - database session time zone
|
84
|
+
# (it is recommended to set it using ENV['TZ'] which will be then also used for database session time zone)
|
85
|
+
#
|
86
|
+
# Optionals NLS parameters:
|
87
|
+
#
|
88
|
+
# * <tt>:nls_calendar</tt>
|
89
|
+
# * <tt>:nls_characterset</tt>
|
90
|
+
# * <tt>:nls_comp</tt>
|
91
|
+
# * <tt>:nls_currency</tt>
|
92
|
+
# * <tt>:nls_date_format</tt> - format for :date columns, defaults to <tt>YYYY-MM-DD HH24:MI:SS</tt>
|
93
|
+
# * <tt>:nls_date_language</tt>
|
94
|
+
# * <tt>:nls_dual_currency</tt>
|
95
|
+
# * <tt>:nls_iso_currency</tt>
|
96
|
+
# * <tt>:nls_language</tt>
|
97
|
+
# * <tt>:nls_length_semantics</tt> - semantics of size of VARCHAR2 and CHAR columns, defaults to <tt>CHAR</tt>
|
98
|
+
# (meaning that size specifies number of characters and not bytes)
|
99
|
+
# * <tt>:nls_nchar_characterset</tt>
|
100
|
+
# * <tt>:nls_nchar_conv_excp</tt>
|
101
|
+
# * <tt>:nls_numeric_characters</tt>
|
102
|
+
# * <tt>:nls_sort</tt>
|
103
|
+
# * <tt>:nls_territory</tt>
|
104
|
+
# * <tt>:nls_timestamp_format</tt> - format for :timestamp columns, defaults to <tt>YYYY-MM-DD HH24:MI:SS:FF6</tt>
|
105
|
+
# * <tt>:nls_timestamp_tz_format</tt>
|
106
|
+
# * <tt>:nls_time_format</tt>
|
107
|
+
# * <tt>:nls_time_tz_format</tt>
|
108
|
+
#
|
109
|
+
class OracleEnhancedAdapter < AbstractAdapter
|
110
|
+
|
111
|
+
##
|
112
|
+
# :singleton-method:
|
113
|
+
# By default, the OracleEnhancedAdapter will consider all columns of type <tt>NUMBER(1)</tt>
|
114
|
+
# as boolean. If you wish to disable this emulation you can add the following line
|
115
|
+
# to your initializer file:
|
116
|
+
#
|
117
|
+
# ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_booleans = false
|
118
|
+
cattr_accessor :emulate_booleans
|
119
|
+
self.emulate_booleans = true
|
120
|
+
|
121
|
+
##
|
122
|
+
# :singleton-method:
|
123
|
+
# By default, the OracleEnhancedAdapter will typecast all columns of type <tt>DATE</tt>
|
124
|
+
# to Time or DateTime (if value is out of Time value range) value.
|
125
|
+
# If you wish that DATE values with hour, minutes and seconds equal to 0 are typecasted
|
126
|
+
# to Date then you can add the following line to your initializer file:
|
127
|
+
#
|
128
|
+
# ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_dates = true
|
129
|
+
#
|
130
|
+
# As this option can have side effects when unnecessary typecasting is done it is recommended
|
131
|
+
# that Date columns are explicily defined with +set_date_columns+ method.
|
132
|
+
cattr_accessor :emulate_dates
|
133
|
+
self.emulate_dates = false
|
134
|
+
|
135
|
+
##
|
136
|
+
# :singleton-method:
|
137
|
+
# OracleEnhancedAdapter will use the default tablespace, but if you want specific types of
|
138
|
+
# objects to go into specific tablespaces, specify them like this in an initializer:
|
139
|
+
#
|
140
|
+
# ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.default_tablespaces =
|
141
|
+
# {:clob => 'TS_LOB', :blob => 'TS_LOB', :index => 'TS_INDEX', :table => 'TS_DATA'}
|
142
|
+
#
|
143
|
+
# Using the :tablespace option where available (e.g create_table) will take precedence
|
144
|
+
# over these settings.
|
145
|
+
cattr_accessor :default_tablespaces
|
146
|
+
self.default_tablespaces={}
|
147
|
+
|
148
|
+
##
|
149
|
+
# :singleton-method:
|
150
|
+
# By default, the OracleEnhancedAdapter will typecast all columns of type <tt>DATE</tt>
|
151
|
+
# to Time or DateTime (if value is out of Time value range) value.
|
152
|
+
# If you wish that DATE columns with "date" in their name (e.g. "creation_date") are typecasted
|
153
|
+
# to Date then you can add the following line to your initializer file:
|
154
|
+
#
|
155
|
+
# ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_dates_by_column_name = true
|
156
|
+
#
|
157
|
+
# As this option can have side effects when unnecessary typecasting is done it is recommended
|
158
|
+
# that Date columns are explicily defined with +set_date_columns+ method.
|
159
|
+
cattr_accessor :emulate_dates_by_column_name
|
160
|
+
self.emulate_dates_by_column_name = false
|
161
|
+
|
162
|
+
# Check column name to identify if it is Date (and not Time) column.
|
163
|
+
# Is used if +emulate_dates_by_column_name+ option is set to +true+.
|
164
|
+
# Override this method definition in initializer file if different Date column recognition is needed.
|
165
|
+
def self.is_date_column?(name, table_name = nil)
|
166
|
+
name =~ /(^|_)date(_|$)/i
|
167
|
+
end
|
168
|
+
|
169
|
+
# instance method uses at first check if column type defined at class level
|
170
|
+
def is_date_column?(name, table_name = nil) #:nodoc:
|
171
|
+
case get_type_for_column(table_name, name)
|
172
|
+
when nil
|
173
|
+
self.class.is_date_column?(name, table_name)
|
174
|
+
when :date
|
175
|
+
true
|
176
|
+
else
|
177
|
+
false
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
##
|
182
|
+
# :singleton-method:
|
183
|
+
# By default, the OracleEnhancedAdapter will typecast all columns of type <tt>NUMBER</tt>
|
184
|
+
# (without precision or scale) to Float or BigDecimal value.
|
185
|
+
# If you wish that NUMBER columns with name "id" or that end with "_id" are typecasted
|
186
|
+
# to Integer then you can add the following line to your initializer file:
|
187
|
+
#
|
188
|
+
# ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_integers_by_column_name = true
|
189
|
+
cattr_accessor :emulate_integers_by_column_name
|
190
|
+
self.emulate_integers_by_column_name = false
|
191
|
+
|
192
|
+
# Check column name to identify if it is Integer (and not Float or BigDecimal) column.
|
193
|
+
# Is used if +emulate_integers_by_column_name+ option is set to +true+.
|
194
|
+
# Override this method definition in initializer file if different Integer column recognition is needed.
|
195
|
+
def self.is_integer_column?(name, table_name = nil)
|
196
|
+
name =~ /(^|_)id$/i
|
197
|
+
end
|
198
|
+
|
199
|
+
##
|
200
|
+
# :singleton-method:
|
201
|
+
# If you wish that CHAR(1), VARCHAR2(1) columns or VARCHAR2 columns with FLAG or YN at the end of their name
|
202
|
+
# are typecasted to booleans then you can add the following line to your initializer file:
|
203
|
+
#
|
204
|
+
# ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_booleans_from_strings = true
|
205
|
+
cattr_accessor :emulate_booleans_from_strings
|
206
|
+
self.emulate_booleans_from_strings = false
|
207
|
+
|
208
|
+
# Check column name to identify if it is boolean (and not String) column.
|
209
|
+
# Is used if +emulate_booleans_from_strings+ option is set to +true+.
|
210
|
+
# Override this method definition in initializer file if different boolean column recognition is needed.
|
211
|
+
def self.is_boolean_column?(name, field_type, table_name = nil)
|
212
|
+
return true if ["CHAR(1)","VARCHAR2(1)"].include?(field_type)
|
213
|
+
field_type =~ /^VARCHAR2/ && (name =~ /_flag$/i || name =~ /_yn$/i)
|
214
|
+
end
|
215
|
+
|
216
|
+
# How boolean value should be quoted to String.
|
217
|
+
# Used if +emulate_booleans_from_strings+ option is set to +true+.
|
218
|
+
def self.boolean_to_string(bool)
|
219
|
+
bool ? "Y" : "N"
|
220
|
+
end
|
221
|
+
|
222
|
+
##
|
223
|
+
# :singleton-method:
|
224
|
+
# Specify non-default date format that should be used when assigning string values to :date columns, e.g.:
|
225
|
+
#
|
226
|
+
# ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.string_to_date_format = “%d.%m.%Y”
|
227
|
+
cattr_accessor :string_to_date_format
|
228
|
+
self.string_to_date_format = nil
|
229
|
+
|
230
|
+
##
|
231
|
+
# :singleton-method:
|
232
|
+
# Specify non-default time format that should be used when assigning string values to :datetime columns, e.g.:
|
233
|
+
#
|
234
|
+
# ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.string_to_time_format = “%d.%m.%Y %H:%M:%S”
|
235
|
+
cattr_accessor :string_to_time_format
|
236
|
+
self.string_to_time_format = nil
|
237
|
+
|
238
|
+
class StatementPool
|
239
|
+
include Enumerable
|
240
|
+
|
241
|
+
def initialize(connection, max = 300)
|
242
|
+
@connection = connection
|
243
|
+
@max = max
|
244
|
+
@cache = {}
|
245
|
+
end
|
246
|
+
|
247
|
+
def each(&block); @cache.each(&block); end
|
248
|
+
def key?(key); @cache.key?(key); end
|
249
|
+
def [](key); @cache[key]; end
|
250
|
+
def length; @cache.length; end
|
251
|
+
def delete(key); @cache.delete(key); end
|
252
|
+
|
253
|
+
def []=(sql, key)
|
254
|
+
while @max <= @cache.size
|
255
|
+
@cache.shift.last.close
|
256
|
+
end
|
257
|
+
@cache[sql] = key
|
258
|
+
end
|
259
|
+
|
260
|
+
def clear
|
261
|
+
@cache.values.each do |cursor|
|
262
|
+
cursor.close
|
263
|
+
end
|
264
|
+
@cache.clear
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def initialize(connection, logger, config) #:nodoc:
|
269
|
+
super(connection, logger)
|
270
|
+
@quoted_column_names, @quoted_table_names = {}, {}
|
271
|
+
@config = config
|
272
|
+
@statements = StatementPool.new(connection, config.fetch(:statement_limit) { 250 })
|
273
|
+
@enable_dbms_output = false
|
274
|
+
@visitor = Arel::Visitors::Oracle.new self if defined?(Arel::Visitors::Oracle)
|
275
|
+
end
|
276
|
+
|
277
|
+
def self.visitor_for(pool) # :nodoc:
|
278
|
+
Arel::Visitors::Oracle.new(pool)
|
279
|
+
end
|
280
|
+
|
281
|
+
ADAPTER_NAME = 'OracleEnhanced'.freeze
|
282
|
+
|
283
|
+
def adapter_name #:nodoc:
|
284
|
+
ADAPTER_NAME
|
285
|
+
end
|
286
|
+
|
287
|
+
def supports_migrations? #:nodoc:
|
288
|
+
true
|
289
|
+
end
|
290
|
+
|
291
|
+
def supports_primary_key? #:nodoc:
|
292
|
+
true
|
293
|
+
end
|
294
|
+
|
295
|
+
def supports_savepoints? #:nodoc:
|
296
|
+
true
|
297
|
+
end
|
298
|
+
|
299
|
+
#:stopdoc:
|
300
|
+
DEFAULT_NLS_PARAMETERS = {
|
301
|
+
:nls_calendar => nil,
|
302
|
+
:nls_characterset => nil,
|
303
|
+
:nls_comp => nil,
|
304
|
+
:nls_currency => nil,
|
305
|
+
:nls_date_format => 'YYYY-MM-DD HH24:MI:SS',
|
306
|
+
:nls_date_language => nil,
|
307
|
+
:nls_dual_currency => nil,
|
308
|
+
:nls_iso_currency => nil,
|
309
|
+
:nls_language => nil,
|
310
|
+
:nls_length_semantics => 'CHAR',
|
311
|
+
:nls_nchar_characterset => nil,
|
312
|
+
:nls_nchar_conv_excp => nil,
|
313
|
+
:nls_numeric_characters => nil,
|
314
|
+
:nls_sort => nil,
|
315
|
+
:nls_territory => nil,
|
316
|
+
:nls_timestamp_format => 'YYYY-MM-DD HH24:MI:SS:FF6',
|
317
|
+
:nls_timestamp_tz_format => nil,
|
318
|
+
:nls_time_format => nil,
|
319
|
+
:nls_time_tz_format => nil
|
320
|
+
}
|
321
|
+
|
322
|
+
#:stopdoc:
|
323
|
+
NATIVE_DATABASE_TYPES = {
|
324
|
+
:primary_key => "NUMBER(38) NOT NULL PRIMARY KEY",
|
325
|
+
:string => { :name => "VARCHAR2", :limit => 255 },
|
326
|
+
:text => { :name => "CLOB" },
|
327
|
+
:integer => { :name => "NUMBER", :limit => 38 },
|
328
|
+
:float => { :name => "NUMBER" },
|
329
|
+
:decimal => { :name => "DECIMAL" },
|
330
|
+
:datetime => { :name => "DATE" },
|
331
|
+
# changed to native TIMESTAMP type
|
332
|
+
# :timestamp => { :name => "DATE" },
|
333
|
+
:timestamp => { :name => "TIMESTAMP" },
|
334
|
+
:time => { :name => "DATE" },
|
335
|
+
:date => { :name => "DATE" },
|
336
|
+
:binary => { :name => "BLOB" },
|
337
|
+
:boolean => { :name => "NUMBER", :limit => 1 },
|
338
|
+
:raw => { :name => "RAW", :limit => 2000 }
|
339
|
+
}
|
340
|
+
# if emulate_booleans_from_strings then store booleans in VARCHAR2
|
341
|
+
NATIVE_DATABASE_TYPES_BOOLEAN_STRINGS = NATIVE_DATABASE_TYPES.dup.merge(
|
342
|
+
:boolean => { :name => "VARCHAR2", :limit => 1 }
|
343
|
+
)
|
344
|
+
#:startdoc:
|
345
|
+
|
346
|
+
def native_database_types #:nodoc:
|
347
|
+
emulate_booleans_from_strings ? NATIVE_DATABASE_TYPES_BOOLEAN_STRINGS : NATIVE_DATABASE_TYPES
|
348
|
+
end
|
349
|
+
|
350
|
+
# maximum length of Oracle identifiers
|
351
|
+
IDENTIFIER_MAX_LENGTH = 30
|
352
|
+
|
353
|
+
def table_alias_length #:nodoc:
|
354
|
+
IDENTIFIER_MAX_LENGTH
|
355
|
+
end
|
356
|
+
|
357
|
+
# the maximum length of a table name
|
358
|
+
def table_name_length
|
359
|
+
IDENTIFIER_MAX_LENGTH
|
360
|
+
end
|
361
|
+
|
362
|
+
# the maximum length of a column name
|
363
|
+
def column_name_length
|
364
|
+
IDENTIFIER_MAX_LENGTH
|
365
|
+
end
|
366
|
+
|
367
|
+
# the maximum length of an index name
|
368
|
+
def index_name_length
|
369
|
+
IDENTIFIER_MAX_LENGTH
|
370
|
+
end
|
371
|
+
|
372
|
+
# the maximum length of a sequence name
|
373
|
+
def sequence_name_length
|
374
|
+
IDENTIFIER_MAX_LENGTH
|
375
|
+
end
|
376
|
+
|
377
|
+
# To avoid ORA-01795: maximum number of expressions in a list is 1000
|
378
|
+
# tell ActiveRecord to limit us to 1000 ids at a time
|
379
|
+
def in_clause_length
|
380
|
+
1000
|
381
|
+
end
|
382
|
+
alias ids_in_list_limit in_clause_length
|
383
|
+
|
384
|
+
# QUOTING ==================================================
|
385
|
+
#
|
386
|
+
# see: abstract/quoting.rb
|
387
|
+
|
388
|
+
def quote_column_name(name) #:nodoc:
|
389
|
+
name = name.to_s
|
390
|
+
@quoted_column_names[name] ||= begin
|
391
|
+
# if only valid lowercase column characters in name
|
392
|
+
if name =~ /\A[a-z][a-z_0-9\$#]*\Z/
|
393
|
+
"\"#{name.upcase}\""
|
394
|
+
else
|
395
|
+
# remove double quotes which cannot be used inside quoted identifier
|
396
|
+
"\"#{name.gsub('"', '')}\""
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
# This method is used in add_index to identify either column name (which is quoted)
|
402
|
+
# or function based index (in which case function expression is not quoted)
|
403
|
+
def quote_column_name_or_expression(name) #:nodoc:
|
404
|
+
name = name.to_s
|
405
|
+
case name
|
406
|
+
# if only valid lowercase column characters in name
|
407
|
+
when /^[a-z][a-z_0-9\$#]*$/
|
408
|
+
"\"#{name.upcase}\""
|
409
|
+
when /^[a-z][a-z_0-9\$#\-]*$/i
|
410
|
+
"\"#{name}\""
|
411
|
+
# if other characters present then assume that it is expression
|
412
|
+
# which should not be quoted
|
413
|
+
else
|
414
|
+
name
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
# Names must be from 1 to 30 bytes long with these exceptions:
|
419
|
+
# * Names of databases are limited to 8 bytes.
|
420
|
+
# * Names of database links can be as long as 128 bytes.
|
421
|
+
#
|
422
|
+
# Nonquoted identifiers cannot be Oracle Database reserved words
|
423
|
+
#
|
424
|
+
# Nonquoted identifiers must begin with an alphabetic character from
|
425
|
+
# your database character set
|
426
|
+
#
|
427
|
+
# Nonquoted identifiers can contain only alphanumeric characters from
|
428
|
+
# your database character set and the underscore (_), dollar sign ($),
|
429
|
+
# and pound sign (#). Database links can also contain periods (.) and
|
430
|
+
# "at" signs (@). Oracle strongly discourages you from using $ and # in
|
431
|
+
# nonquoted identifiers.
|
432
|
+
NONQUOTED_OBJECT_NAME = /[A-Za-z][A-z0-9$#]{0,29}/
|
433
|
+
NONQUOTED_DATABASE_LINK = /[A-Za-z][A-z0-9$#\.@]{0,127}/
|
434
|
+
VALID_TABLE_NAME = /\A(?:#{NONQUOTED_OBJECT_NAME}\.)?#{NONQUOTED_OBJECT_NAME}(?:@#{NONQUOTED_DATABASE_LINK})?\Z/
|
435
|
+
|
436
|
+
# unescaped table name should start with letter and
|
437
|
+
# contain letters, digits, _, $ or #
|
438
|
+
# can be prefixed with schema name
|
439
|
+
# CamelCase table names should be quoted
|
440
|
+
def self.valid_table_name?(name) #:nodoc:
|
441
|
+
name = name.to_s
|
442
|
+
name =~ VALID_TABLE_NAME && !(name =~ /[A-Z]/ && name =~ /[a-z]/) ? true : false
|
443
|
+
end
|
444
|
+
|
445
|
+
def quote_table_name(name) #:nodoc:
|
446
|
+
name = name.to_s
|
447
|
+
@quoted_table_names[name] ||= name.split('.').map{|n| n.split('@').map{|m| quote_column_name(m)}.join('@')}.join('.')
|
448
|
+
end
|
449
|
+
|
450
|
+
def quote_string(s) #:nodoc:
|
451
|
+
s.gsub(/'/, "''")
|
452
|
+
end
|
453
|
+
|
454
|
+
def quote(value, column = nil) #:nodoc:
|
455
|
+
if value && column
|
456
|
+
case column.type
|
457
|
+
when :text, :binary
|
458
|
+
%Q{empty_#{ column.sql_type.downcase rescue 'blob' }()}
|
459
|
+
# NLS_DATE_FORMAT independent TIMESTAMP support
|
460
|
+
when :timestamp
|
461
|
+
quote_timestamp_with_to_timestamp(value)
|
462
|
+
# NLS_DATE_FORMAT independent DATE support
|
463
|
+
when :date, :time, :datetime
|
464
|
+
quote_date_with_to_date(value)
|
465
|
+
when :raw
|
466
|
+
quote_raw(value)
|
467
|
+
when :string
|
468
|
+
# NCHAR and NVARCHAR2 literals should be quoted with N'...'.
|
469
|
+
# Read directly instance variable as otherwise migrations with table column default values are failing
|
470
|
+
# as migrations pass ColumnDefinition object to this method.
|
471
|
+
# Check if instance variable is defined to avoid warnings about accessing undefined instance variable.
|
472
|
+
column.instance_variable_defined?('@nchar') && column.instance_variable_get('@nchar') ? 'N' << super : super
|
473
|
+
else
|
474
|
+
super
|
475
|
+
end
|
476
|
+
elsif value.acts_like?(:date)
|
477
|
+
quote_date_with_to_date(value)
|
478
|
+
elsif value.acts_like?(:time)
|
479
|
+
value.to_i == value.to_f ? quote_date_with_to_date(value) : quote_timestamp_with_to_timestamp(value)
|
480
|
+
else
|
481
|
+
super
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
def quoted_true #:nodoc:
|
486
|
+
return "'#{self.class.boolean_to_string(true)}'" if emulate_booleans_from_strings
|
487
|
+
"1"
|
488
|
+
end
|
489
|
+
|
490
|
+
def quoted_false #:nodoc:
|
491
|
+
return "'#{self.class.boolean_to_string(false)}'" if emulate_booleans_from_strings
|
492
|
+
"0"
|
493
|
+
end
|
494
|
+
|
495
|
+
def quote_date_with_to_date(value) #:nodoc:
|
496
|
+
# should support that composite_primary_keys gem will pass date as string
|
497
|
+
value = quoted_date(value) if value.acts_like?(:date) || value.acts_like?(:time)
|
498
|
+
"TO_DATE('#{value}','YYYY-MM-DD HH24:MI:SS')"
|
499
|
+
end
|
500
|
+
|
501
|
+
# Encode a string or byte array as string of hex codes
|
502
|
+
def self.encode_raw(value)
|
503
|
+
# When given a string, convert to a byte array.
|
504
|
+
value = value.unpack('C*') if value.is_a?(String)
|
505
|
+
value.map { |x| "%02X" % x }.join
|
506
|
+
end
|
507
|
+
|
508
|
+
# quote encoded raw value
|
509
|
+
def quote_raw(value) #:nodoc:
|
510
|
+
"'#{self.class.encode_raw(value)}'"
|
511
|
+
end
|
512
|
+
|
513
|
+
def quote_timestamp_with_to_timestamp(value) #:nodoc:
|
514
|
+
# add up to 9 digits of fractional seconds to inserted time
|
515
|
+
value = "#{quoted_date(value)}:#{("%.6f"%value.to_f).split('.')[1]}" if value.acts_like?(:time)
|
516
|
+
"TO_TIMESTAMP('#{value}','YYYY-MM-DD HH24:MI:SS:FF6')"
|
517
|
+
end
|
518
|
+
|
519
|
+
# Cast a +value+ to a type that the database understands.
|
520
|
+
def type_cast(value, column)
|
521
|
+
case value
|
522
|
+
when true, false
|
523
|
+
if emulate_booleans_from_strings || column && column.type == :string
|
524
|
+
self.class.boolean_to_string(value)
|
525
|
+
else
|
526
|
+
value ? 1 : 0
|
527
|
+
end
|
528
|
+
when Date, Time
|
529
|
+
if value.acts_like?(:time)
|
530
|
+
zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
|
531
|
+
value.respond_to?(zone_conversion_method) ? value.send(zone_conversion_method) : value
|
532
|
+
else
|
533
|
+
value
|
534
|
+
end
|
535
|
+
else
|
536
|
+
super
|
537
|
+
end
|
538
|
+
end
|
539
|
+
|
540
|
+
# CONNECTION MANAGEMENT ====================================
|
541
|
+
#
|
542
|
+
|
543
|
+
# If SQL statement fails due to lost connection then reconnect
|
544
|
+
# and retry SQL statement if autocommit mode is enabled.
|
545
|
+
# By default this functionality is disabled.
|
546
|
+
attr_reader :auto_retry #:nodoc:
|
547
|
+
@auto_retry = false
|
548
|
+
|
549
|
+
def auto_retry=(value) #:nodoc:
|
550
|
+
@auto_retry = value
|
551
|
+
@connection.auto_retry = value if @connection
|
552
|
+
end
|
553
|
+
|
554
|
+
# return raw OCI8 or JDBC connection
|
555
|
+
def raw_connection
|
556
|
+
@connection.raw_connection
|
557
|
+
end
|
558
|
+
|
559
|
+
# Returns true if the connection is active.
|
560
|
+
def active? #:nodoc:
|
561
|
+
# Pings the connection to check if it's still good. Note that an
|
562
|
+
# #active? method is also available, but that simply returns the
|
563
|
+
# last known state, which isn't good enough if the connection has
|
564
|
+
# gone stale since the last use.
|
565
|
+
@connection.ping
|
566
|
+
rescue OracleEnhancedConnectionException
|
567
|
+
false
|
568
|
+
end
|
569
|
+
|
570
|
+
# Reconnects to the database.
|
571
|
+
def reconnect! #:nodoc:
|
572
|
+
clear_cache!
|
573
|
+
@connection.reset!
|
574
|
+
rescue OracleEnhancedConnectionException => e
|
575
|
+
@logger.warn "#{adapter_name} automatic reconnection failed: #{e.message}" if @logger
|
576
|
+
end
|
577
|
+
|
578
|
+
def reset!
|
579
|
+
clear_cache!
|
580
|
+
super
|
581
|
+
end
|
582
|
+
|
583
|
+
# Disconnects from the database.
|
584
|
+
def disconnect! #:nodoc:
|
585
|
+
clear_cache!
|
586
|
+
@connection.logoff rescue nil
|
587
|
+
end
|
588
|
+
|
589
|
+
# DATABASE STATEMENTS ======================================
|
590
|
+
#
|
591
|
+
# see: abstract/database_statements.rb
|
592
|
+
|
593
|
+
# Executes a SQL statement
|
594
|
+
def execute(sql, name = nil)
|
595
|
+
log(sql, name) { @connection.exec(sql) }
|
596
|
+
end
|
597
|
+
|
598
|
+
def substitute_at(column, index)
|
599
|
+
Arel.sql(":a#{index + 1}")
|
600
|
+
end
|
601
|
+
|
602
|
+
def clear_cache!
|
603
|
+
@statements.clear
|
604
|
+
end
|
605
|
+
|
606
|
+
def exec_query(sql, name = 'SQL', binds = [])
|
607
|
+
log(sql, name, binds) do
|
608
|
+
cursor = nil
|
609
|
+
cached = false
|
610
|
+
if binds.empty?
|
611
|
+
cursor = @connection.prepare(sql)
|
612
|
+
else
|
613
|
+
unless @statements.key? sql
|
614
|
+
@statements[sql] = @connection.prepare(sql)
|
615
|
+
end
|
616
|
+
|
617
|
+
cursor = @statements[sql]
|
618
|
+
|
619
|
+
binds.each_with_index do |bind, i|
|
620
|
+
col, val = bind
|
621
|
+
cursor.bind_param(i + 1, type_cast(val, col), col && col.type)
|
622
|
+
end
|
623
|
+
|
624
|
+
cached = true
|
625
|
+
end
|
626
|
+
|
627
|
+
cursor.exec
|
628
|
+
|
629
|
+
if name == 'EXPLAIN'
|
630
|
+
res = true
|
631
|
+
else
|
632
|
+
columns = cursor.get_col_names.map do |col_name|
|
633
|
+
@connection.oracle_downcase(col_name)
|
634
|
+
end
|
635
|
+
rows = []
|
636
|
+
fetch_options = {:get_lob_value => (name != 'Writable Large Object')}
|
637
|
+
while row = cursor.fetch(fetch_options)
|
638
|
+
rows << row
|
639
|
+
end
|
640
|
+
res = ActiveRecord::Result.new(columns, rows)
|
641
|
+
end
|
642
|
+
|
643
|
+
cursor.close unless cached
|
644
|
+
res
|
645
|
+
end
|
646
|
+
end
|
647
|
+
|
648
|
+
def supports_statement_cache?
|
649
|
+
true
|
650
|
+
end
|
651
|
+
|
652
|
+
def supports_explain?
|
653
|
+
true
|
654
|
+
end
|
655
|
+
|
656
|
+
def explain(arel, binds = [])
|
657
|
+
sql = "EXPLAIN PLAN FOR #{to_sql(arel)}"
|
658
|
+
return if sql =~ /FROM all_/
|
659
|
+
if ORACLE_ENHANCED_CONNECTION == :jdbc
|
660
|
+
exec_query(sql, 'EXPLAIN', binds)
|
661
|
+
else
|
662
|
+
exec_query(sql, 'EXPLAIN')
|
663
|
+
end
|
664
|
+
select_values("SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY)", 'EXPLAIN').join("\n")
|
665
|
+
end
|
666
|
+
|
667
|
+
# Returns an array of arrays containing the field values.
|
668
|
+
# Order is the same as that returned by #columns.
|
669
|
+
def select_rows(sql, name = nil)
|
670
|
+
# last parameter indicates to return also column list
|
671
|
+
result = columns = nil
|
672
|
+
log(sql, name) do
|
673
|
+
result, columns = @connection.select(sql, name, true)
|
674
|
+
end
|
675
|
+
result.map{ |v| columns.map{|c| v[c]} }
|
676
|
+
end
|
677
|
+
|
678
|
+
# Executes an INSERT statement and returns the new record's ID
|
679
|
+
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
|
680
|
+
# if primary key value is already prefetched from sequence
|
681
|
+
# or if there is no primary key
|
682
|
+
if id_value || pk.nil?
|
683
|
+
execute(sql, name)
|
684
|
+
return id_value
|
685
|
+
end
|
686
|
+
|
687
|
+
sql_with_returning = sql + @connection.returning_clause(quote_column_name(pk))
|
688
|
+
log(sql, name) do
|
689
|
+
@connection.exec_with_returning(sql_with_returning)
|
690
|
+
end
|
691
|
+
end
|
692
|
+
protected :insert_sql
|
693
|
+
|
694
|
+
# New method in ActiveRecord 3.1
|
695
|
+
# Will add RETURNING clause in case of trigger generated primary keys
|
696
|
+
def sql_for_insert(sql, pk, id_value, sequence_name, binds)
|
697
|
+
unless id_value || pk.nil? || (defined?(CompositePrimaryKeys) && pk.kind_of?(CompositePrimaryKeys::CompositeKeys))
|
698
|
+
sql = "#{sql} RETURNING #{quote_column_name(pk)} INTO :returning_id"
|
699
|
+
(binds = binds.dup) << [:returning_id, nil]
|
700
|
+
end
|
701
|
+
[sql, binds]
|
702
|
+
end
|
703
|
+
|
704
|
+
EXEC_INSERT_RESULT_COLUMNS = %w(returning_id) #:nodoc:
|
705
|
+
|
706
|
+
# New method in ActiveRecord 3.1
|
707
|
+
def exec_insert(sql, name, binds)
|
708
|
+
log(sql, name, binds) do
|
709
|
+
returning_id_index = nil
|
710
|
+
cursor = if @statements.key?(sql)
|
711
|
+
@statements[sql]
|
712
|
+
else
|
713
|
+
@statements[sql] = @connection.prepare(sql)
|
714
|
+
end
|
715
|
+
|
716
|
+
binds.each_with_index do |bind, i|
|
717
|
+
col, val = bind
|
718
|
+
if col == :returning_id
|
719
|
+
returning_id_index = i + 1
|
720
|
+
cursor.bind_returning_param(returning_id_index, Integer)
|
721
|
+
else
|
722
|
+
cursor.bind_param(i + 1, type_cast(val, col), col && col.type)
|
723
|
+
end
|
724
|
+
end
|
725
|
+
|
726
|
+
cursor.exec_update
|
727
|
+
|
728
|
+
rows = []
|
729
|
+
if returning_id_index
|
730
|
+
returning_id = cursor.get_returning_param(returning_id_index, Integer)
|
731
|
+
rows << [returning_id]
|
732
|
+
end
|
733
|
+
ActiveRecord::Result.new(EXEC_INSERT_RESULT_COLUMNS, rows)
|
734
|
+
end
|
735
|
+
end
|
736
|
+
|
737
|
+
# New method in ActiveRecord 3.1
|
738
|
+
def exec_update(sql, name, binds)
|
739
|
+
log(sql, name, binds) do
|
740
|
+
cached = false
|
741
|
+
if binds.empty?
|
742
|
+
cursor = @connection.prepare(sql)
|
743
|
+
else
|
744
|
+
cursor = if @statements.key?(sql)
|
745
|
+
@statements[sql]
|
746
|
+
else
|
747
|
+
@statements[sql] = @connection.prepare(sql)
|
748
|
+
end
|
749
|
+
|
750
|
+
binds.each_with_index do |bind, i|
|
751
|
+
col, val = bind
|
752
|
+
cursor.bind_param(i + 1, type_cast(val, col), col && col.type)
|
753
|
+
end
|
754
|
+
cached = true
|
755
|
+
end
|
756
|
+
|
757
|
+
res = cursor.exec_update
|
758
|
+
cursor.close unless cached
|
759
|
+
res
|
760
|
+
end
|
761
|
+
end
|
762
|
+
|
763
|
+
alias :exec_delete :exec_update
|
764
|
+
|
765
|
+
# use in set_sequence_name to avoid fetching primary key value from sequence
|
766
|
+
AUTOGENERATED_SEQUENCE_NAME = 'autogenerated'.freeze
|
767
|
+
|
768
|
+
# Returns the next sequence value from a sequence generator. Not generally
|
769
|
+
# called directly; used by ActiveRecord to get the next primary key value
|
770
|
+
# when inserting a new database record (see #prefetch_primary_key?).
|
771
|
+
def next_sequence_value(sequence_name)
|
772
|
+
# if sequence_name is set to :autogenerated then it means that primary key will be populated by trigger
|
773
|
+
return nil if sequence_name == AUTOGENERATED_SEQUENCE_NAME
|
774
|
+
# call directly connection method to avoid prepared statement which causes fetching of next sequence value twice
|
775
|
+
@connection.select_value("SELECT #{quote_table_name(sequence_name)}.NEXTVAL FROM dual")
|
776
|
+
end
|
777
|
+
|
778
|
+
def begin_db_transaction #:nodoc:
|
779
|
+
@connection.autocommit = false
|
780
|
+
end
|
781
|
+
|
782
|
+
def commit_db_transaction #:nodoc:
|
783
|
+
@connection.commit
|
784
|
+
ensure
|
785
|
+
@connection.autocommit = true
|
786
|
+
end
|
787
|
+
|
788
|
+
def rollback_db_transaction #:nodoc:
|
789
|
+
@connection.rollback
|
790
|
+
ensure
|
791
|
+
@connection.autocommit = true
|
792
|
+
end
|
793
|
+
|
794
|
+
def create_savepoint #:nodoc:
|
795
|
+
execute("SAVEPOINT #{current_savepoint_name}")
|
796
|
+
end
|
797
|
+
|
798
|
+
def rollback_to_savepoint #:nodoc:
|
799
|
+
execute("ROLLBACK TO #{current_savepoint_name}")
|
800
|
+
end
|
801
|
+
|
802
|
+
def release_savepoint #:nodoc:
|
803
|
+
# there is no RELEASE SAVEPOINT statement in Oracle
|
804
|
+
end
|
805
|
+
|
806
|
+
def add_limit_offset!(sql, options) #:nodoc:
|
807
|
+
# added to_i for limit and offset to protect from SQL injection
|
808
|
+
offset = (options[:offset] || 0).to_i
|
809
|
+
limit = options[:limit]
|
810
|
+
limit = limit.is_a?(String) && limit.blank? ? nil : limit && limit.to_i
|
811
|
+
if limit && offset > 0
|
812
|
+
sql.replace "SELECT * FROM (SELECT raw_sql_.*, ROWNUM raw_rnum_ FROM (#{sql}) raw_sql_ WHERE ROWNUM <= #{offset+limit}) WHERE raw_rnum_ > #{offset}"
|
813
|
+
elsif limit
|
814
|
+
sql.replace "SELECT * FROM (#{sql}) WHERE ROWNUM <= #{limit}"
|
815
|
+
elsif offset > 0
|
816
|
+
sql.replace "SELECT * FROM (SELECT raw_sql_.*, ROWNUM raw_rnum_ FROM (#{sql}) raw_sql_) WHERE raw_rnum_ > #{offset}"
|
817
|
+
end
|
818
|
+
end
|
819
|
+
|
820
|
+
@@do_not_prefetch_primary_key = {}
|
821
|
+
|
822
|
+
# Returns true for Oracle adapter (since Oracle requires primary key
|
823
|
+
# values to be pre-fetched before insert). See also #next_sequence_value.
|
824
|
+
def prefetch_primary_key?(table_name = nil)
|
825
|
+
return true if table_name.nil?
|
826
|
+
table_name = table_name.to_s
|
827
|
+
do_not_prefetch = @@do_not_prefetch_primary_key[table_name]
|
828
|
+
if do_not_prefetch.nil?
|
829
|
+
owner, desc_table_name, db_link = @connection.describe(table_name)
|
830
|
+
@@do_not_prefetch_primary_key[table_name] = do_not_prefetch =
|
831
|
+
!has_primary_key?(table_name, owner, desc_table_name, db_link) ||
|
832
|
+
has_primary_key_trigger?(table_name, owner, desc_table_name, db_link)
|
833
|
+
end
|
834
|
+
!do_not_prefetch
|
835
|
+
end
|
836
|
+
|
837
|
+
# used just in tests to clear prefetch primary key flag for all tables
|
838
|
+
def clear_prefetch_primary_key #:nodoc:
|
839
|
+
@@do_not_prefetch_primary_key = {}
|
840
|
+
end
|
841
|
+
|
842
|
+
# Returns default sequence name for table.
|
843
|
+
# Will take all or first 26 characters of table name and append _seq suffix
|
844
|
+
def default_sequence_name(table_name, primary_key = nil)
|
845
|
+
# TODO: remove schema prefix if present before truncating
|
846
|
+
# truncate table name if necessary to fit in max length of identifier
|
847
|
+
"#{table_name.to_s[0,IDENTIFIER_MAX_LENGTH-4]}_seq"
|
848
|
+
end
|
849
|
+
|
850
|
+
# Inserts the given fixture into the table. Overridden to properly handle lobs.
|
851
|
+
def insert_fixture(fixture, table_name) #:nodoc:
|
852
|
+
super
|
853
|
+
|
854
|
+
if ActiveRecord::Base.pluralize_table_names
|
855
|
+
klass = table_name.singularize.camelize
|
856
|
+
else
|
857
|
+
klass = table_name.camelize
|
858
|
+
end
|
859
|
+
|
860
|
+
klass = klass.constantize rescue nil
|
861
|
+
if klass.respond_to?(:ancestors) && klass.ancestors.include?(ActiveRecord::Base)
|
862
|
+
write_lobs(table_name, klass, fixture, klass.lob_columns)
|
863
|
+
end
|
864
|
+
end
|
865
|
+
|
866
|
+
# Writes LOB values from attributes for specified columns
|
867
|
+
def write_lobs(table_name, klass, attributes, columns) #:nodoc:
|
868
|
+
# is class with composite primary key>
|
869
|
+
is_with_cpk = klass.respond_to?(:composite?) && klass.composite?
|
870
|
+
if is_with_cpk
|
871
|
+
id = klass.primary_key.map {|pk| attributes[pk.to_s] }
|
872
|
+
else
|
873
|
+
id = quote(attributes[klass.primary_key])
|
874
|
+
end
|
875
|
+
columns.each do |col|
|
876
|
+
value = attributes[col.name]
|
877
|
+
# changed sequence of next two lines - should check if value is nil before converting to yaml
|
878
|
+
next if value.nil? || (value == '')
|
879
|
+
value = value.to_yaml if col.text? && klass.serialized_attributes[col.name]
|
880
|
+
uncached do
|
881
|
+
sql = is_with_cpk ? "SELECT #{quote_column_name(col.name)} FROM #{quote_table_name(table_name)} WHERE #{klass.composite_where_clause(id)} FOR UPDATE" :
|
882
|
+
"SELECT #{quote_column_name(col.name)} FROM #{quote_table_name(table_name)} WHERE #{quote_column_name(klass.primary_key)} = #{id} FOR UPDATE"
|
883
|
+
unless lob_record = select_one(sql, 'Writable Large Object')
|
884
|
+
raise ActiveRecord::RecordNotFound, "statement #{sql} returned no rows"
|
885
|
+
end
|
886
|
+
lob = lob_record[col.name]
|
887
|
+
@connection.write_lob(lob, value.to_s, col.type == :binary)
|
888
|
+
end
|
889
|
+
end
|
890
|
+
end
|
891
|
+
|
892
|
+
# Current database name
|
893
|
+
def current_database
|
894
|
+
select_value("SELECT SYS_CONTEXT('userenv', 'db_name') FROM dual")
|
895
|
+
end
|
896
|
+
|
897
|
+
# Current database session user
|
898
|
+
def current_user
|
899
|
+
select_value("SELECT SYS_CONTEXT('userenv', 'session_user') FROM dual")
|
900
|
+
end
|
901
|
+
|
902
|
+
# Current database session schema
|
903
|
+
def current_schema
|
904
|
+
select_value("SELECT SYS_CONTEXT('userenv', 'current_schema') FROM dual")
|
905
|
+
end
|
906
|
+
|
907
|
+
# Default tablespace name of current user
|
908
|
+
def default_tablespace
|
909
|
+
select_value("SELECT LOWER(default_tablespace) FROM user_users WHERE username = SYS_CONTEXT('userenv', 'current_schema')")
|
910
|
+
end
|
911
|
+
|
912
|
+
def tables(name = nil) #:nodoc:
|
913
|
+
select_values(
|
914
|
+
"SELECT DECODE(table_name, UPPER(table_name), LOWER(table_name), table_name) FROM all_tables WHERE owner = SYS_CONTEXT('userenv', 'current_schema') AND secondary = 'N'",
|
915
|
+
name)
|
916
|
+
end
|
917
|
+
|
918
|
+
# Will return true if database object exists (to be able to use also views and synonyms for ActiveRecord models)
|
919
|
+
def table_exists?(table_name)
|
920
|
+
(owner, table_name, db_link) = @connection.describe(table_name)
|
921
|
+
true
|
922
|
+
rescue
|
923
|
+
false
|
924
|
+
end
|
925
|
+
|
926
|
+
def materialized_views #:nodoc:
|
927
|
+
select_values("SELECT LOWER(mview_name) FROM all_mviews WHERE owner = SYS_CONTEXT('userenv', 'current_schema')")
|
928
|
+
end
|
929
|
+
|
930
|
+
cattr_accessor :all_schema_indexes #:nodoc:
|
931
|
+
|
932
|
+
# This method selects all indexes at once, and caches them in a class variable.
|
933
|
+
# Subsequent index calls get them from the variable, without going to the DB.
|
934
|
+
def indexes(table_name, name = nil) #:nodoc:
|
935
|
+
(owner, table_name, db_link) = @connection.describe(table_name)
|
936
|
+
unless all_schema_indexes
|
937
|
+
default_tablespace_name = default_tablespace
|
938
|
+
result = select_all(<<-SQL.strip.gsub(/\s+/, ' '))
|
939
|
+
SELECT LOWER(i.table_name) AS table_name, LOWER(i.index_name) AS index_name, i.uniqueness,
|
940
|
+
i.index_type, i.ityp_owner, i.ityp_name, i.parameters,
|
941
|
+
LOWER(i.tablespace_name) AS tablespace_name,
|
942
|
+
LOWER(c.column_name) AS column_name, e.column_expression,
|
943
|
+
atc.virtual_column
|
944
|
+
FROM all_indexes#{db_link} i
|
945
|
+
JOIN all_ind_columns#{db_link} c ON c.index_name = i.index_name AND c.index_owner = i.owner
|
946
|
+
LEFT OUTER JOIN all_ind_expressions#{db_link} e ON e.index_name = i.index_name AND
|
947
|
+
e.index_owner = i.owner AND e.column_position = c.column_position
|
948
|
+
LEFT OUTER JOIN all_tab_cols#{db_link} atc ON i.table_name = atc.table_name AND
|
949
|
+
c.column_name = atc.column_name AND i.owner = atc.owner AND atc.hidden_column = 'NO'
|
950
|
+
WHERE i.owner = '#{owner}'
|
951
|
+
AND i.table_owner = '#{owner}'
|
952
|
+
AND NOT EXISTS (SELECT uc.index_name FROM all_constraints uc
|
953
|
+
WHERE uc.index_name = i.index_name AND uc.owner = i.owner AND uc.constraint_type = 'P')
|
954
|
+
ORDER BY i.index_name, c.column_position
|
955
|
+
SQL
|
956
|
+
|
957
|
+
current_index = nil
|
958
|
+
self.all_schema_indexes = []
|
959
|
+
|
960
|
+
result.each do |row|
|
961
|
+
# have to keep track of indexes because above query returns dups
|
962
|
+
# there is probably a better query we could figure out
|
963
|
+
if current_index != row['index_name']
|
964
|
+
statement_parameters = nil
|
965
|
+
if row['index_type'] == 'DOMAIN' && row['ityp_owner'] == 'CTXSYS' && row['ityp_name'] == 'CONTEXT'
|
966
|
+
procedure_name = default_datastore_procedure(row['index_name'])
|
967
|
+
source = select_values(<<-SQL).join
|
968
|
+
SELECT text
|
969
|
+
FROM all_source#{db_link}
|
970
|
+
WHERE owner = '#{owner}'
|
971
|
+
AND name = '#{procedure_name.upcase}'
|
972
|
+
ORDER BY line
|
973
|
+
SQL
|
974
|
+
if source =~ /-- add_context_index_parameters (.+)\n/
|
975
|
+
statement_parameters = $1
|
976
|
+
end
|
977
|
+
end
|
978
|
+
all_schema_indexes << OracleEnhancedIndexDefinition.new(row['table_name'], row['index_name'],
|
979
|
+
row['uniqueness'] == "UNIQUE", row['index_type'] == 'DOMAIN' ? "#{row['ityp_owner']}.#{row['ityp_name']}" : nil,
|
980
|
+
row['parameters'], statement_parameters,
|
981
|
+
row['tablespace_name'] == default_tablespace_name ? nil : row['tablespace_name'], [])
|
982
|
+
current_index = row['index_name']
|
983
|
+
end
|
984
|
+
|
985
|
+
# Functional index columns and virtual columns both get stored as column expressions,
|
986
|
+
# but re-creating a virtual column index as an expression (instead of using the virtual column's name)
|
987
|
+
# results in a ORA-54018 error. Thus, we only want the column expression value returned
|
988
|
+
# when the column is not virtual.
|
989
|
+
if row['column_expression'] && row['virtual_column'] != 'YES'
|
990
|
+
all_schema_indexes.last.columns << row['column_expression']
|
991
|
+
else
|
992
|
+
all_schema_indexes.last.columns << row['column_name'].downcase
|
993
|
+
end
|
994
|
+
end
|
995
|
+
end
|
996
|
+
|
997
|
+
# Return the indexes just for the requested table, since AR is structured that way
|
998
|
+
table_name = table_name.downcase
|
999
|
+
all_schema_indexes.select{|i| i.table == table_name}
|
1000
|
+
end
|
1001
|
+
|
1002
|
+
@@ignore_table_columns = nil #:nodoc:
|
1003
|
+
|
1004
|
+
# set ignored columns for table
|
1005
|
+
def ignore_table_columns(table_name, *args) #:nodoc:
|
1006
|
+
@@ignore_table_columns ||= {}
|
1007
|
+
@@ignore_table_columns[table_name] ||= []
|
1008
|
+
@@ignore_table_columns[table_name] += args.map{|a| a.to_s.downcase}
|
1009
|
+
@@ignore_table_columns[table_name].uniq!
|
1010
|
+
end
|
1011
|
+
|
1012
|
+
def ignored_table_columns(table_name) #:nodoc:
|
1013
|
+
@@ignore_table_columns ||= {}
|
1014
|
+
@@ignore_table_columns[table_name]
|
1015
|
+
end
|
1016
|
+
|
1017
|
+
# used just in tests to clear ignored table columns
|
1018
|
+
def clear_ignored_table_columns #:nodoc:
|
1019
|
+
@@ignore_table_columns = nil
|
1020
|
+
end
|
1021
|
+
|
1022
|
+
@@table_column_type = nil #:nodoc:
|
1023
|
+
|
1024
|
+
# set explicit type for specified table columns
|
1025
|
+
def set_type_for_columns(table_name, column_type, *args) #:nodoc:
|
1026
|
+
@@table_column_type ||= {}
|
1027
|
+
@@table_column_type[table_name] ||= {}
|
1028
|
+
args.each do |col|
|
1029
|
+
@@table_column_type[table_name][col.to_s.downcase] = column_type
|
1030
|
+
end
|
1031
|
+
end
|
1032
|
+
|
1033
|
+
def get_type_for_column(table_name, column_name) #:nodoc:
|
1034
|
+
@@table_column_type && @@table_column_type[table_name] && @@table_column_type[table_name][column_name.to_s.downcase]
|
1035
|
+
end
|
1036
|
+
|
1037
|
+
# used just in tests to clear column data type definitions
|
1038
|
+
def clear_types_for_columns #:nodoc:
|
1039
|
+
@@table_column_type = nil
|
1040
|
+
end
|
1041
|
+
|
1042
|
+
# check if table has primary key trigger with _pkt suffix
|
1043
|
+
def has_primary_key_trigger?(table_name, owner = nil, desc_table_name = nil, db_link = nil)
|
1044
|
+
(owner, desc_table_name, db_link) = @connection.describe(table_name) unless owner
|
1045
|
+
|
1046
|
+
trigger_name = default_trigger_name(table_name).upcase
|
1047
|
+
pkt_sql = <<-SQL
|
1048
|
+
SELECT trigger_name
|
1049
|
+
FROM all_triggers#{db_link}
|
1050
|
+
WHERE owner = '#{owner}'
|
1051
|
+
AND trigger_name = '#{trigger_name}'
|
1052
|
+
AND table_owner = '#{owner}'
|
1053
|
+
AND table_name = '#{desc_table_name}'
|
1054
|
+
AND status = 'ENABLED'
|
1055
|
+
SQL
|
1056
|
+
select_value(pkt_sql, 'Primary Key Trigger') ? true : false
|
1057
|
+
end
|
1058
|
+
|
1059
|
+
##
|
1060
|
+
# :singleton-method:
|
1061
|
+
# Cache column description between requests.
|
1062
|
+
# Could be used in development environment to avoid selecting table columns from data dictionary tables for each request.
|
1063
|
+
# This can speed up request processing in development mode if development database is not on local computer.
|
1064
|
+
#
|
1065
|
+
# ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.cache_columns = true
|
1066
|
+
cattr_accessor :cache_columns
|
1067
|
+
self.cache_columns = false
|
1068
|
+
|
1069
|
+
def columns(table_name, name = nil) #:nodoc:
|
1070
|
+
if @@cache_columns
|
1071
|
+
@@columns_cache ||= {}
|
1072
|
+
@@columns_cache[table_name] ||= columns_without_cache(table_name, name)
|
1073
|
+
else
|
1074
|
+
columns_without_cache(table_name, name)
|
1075
|
+
end
|
1076
|
+
end
|
1077
|
+
|
1078
|
+
def columns_without_cache(table_name, name = nil) #:nodoc:
|
1079
|
+
table_name = table_name.to_s
|
1080
|
+
# get ignored_columns by original table name
|
1081
|
+
ignored_columns = ignored_table_columns(table_name)
|
1082
|
+
|
1083
|
+
(owner, desc_table_name, db_link) = @connection.describe(table_name)
|
1084
|
+
|
1085
|
+
# reset do_not_prefetch_primary_key cache for this table
|
1086
|
+
@@do_not_prefetch_primary_key[table_name] = nil
|
1087
|
+
|
1088
|
+
table_cols = <<-SQL.strip.gsub(/\s+/, ' ')
|
1089
|
+
SELECT column_name AS name, data_type AS sql_type, data_default, nullable, virtual_column, hidden_column,
|
1090
|
+
DECODE(data_type, 'NUMBER', data_precision,
|
1091
|
+
'FLOAT', data_precision,
|
1092
|
+
'VARCHAR2', DECODE(char_used, 'C', char_length, data_length),
|
1093
|
+
'RAW', DECODE(char_used, 'C', char_length, data_length),
|
1094
|
+
'CHAR', DECODE(char_used, 'C', char_length, data_length),
|
1095
|
+
NULL) AS limit,
|
1096
|
+
DECODE(data_type, 'NUMBER', data_scale, NULL) AS scale
|
1097
|
+
FROM all_tab_cols#{db_link}
|
1098
|
+
WHERE owner = '#{owner}'
|
1099
|
+
AND table_name = '#{desc_table_name}'
|
1100
|
+
AND hidden_column = 'NO'
|
1101
|
+
ORDER BY column_id
|
1102
|
+
SQL
|
1103
|
+
|
1104
|
+
# added deletion of ignored columns
|
1105
|
+
select_all(table_cols, name).delete_if do |row|
|
1106
|
+
ignored_columns && ignored_columns.include?(row['name'].downcase)
|
1107
|
+
end.map do |row|
|
1108
|
+
limit, scale = row['limit'], row['scale']
|
1109
|
+
if limit || scale
|
1110
|
+
row['sql_type'] += "(#{(limit || 38).to_i}" + ((scale = scale.to_i) > 0 ? ",#{scale})" : ")")
|
1111
|
+
end
|
1112
|
+
|
1113
|
+
is_virtual = row['virtual_column']=='YES'
|
1114
|
+
|
1115
|
+
# clean up odd default spacing from Oracle
|
1116
|
+
if row['data_default'] && !is_virtual
|
1117
|
+
row['data_default'].sub!(/^(.*?)\s*$/, '\1')
|
1118
|
+
|
1119
|
+
# If a default contains a newline these cleanup regexes need to
|
1120
|
+
# match newlines.
|
1121
|
+
row['data_default'].sub!(/^'(.*)'$/m, '\1')
|
1122
|
+
row['data_default'] = nil if row['data_default'] =~ /^(null|empty_[bc]lob\(\))$/i
|
1123
|
+
end
|
1124
|
+
|
1125
|
+
OracleEnhancedColumn.new(oracle_downcase(row['name']),
|
1126
|
+
row['data_default'],
|
1127
|
+
row['sql_type'],
|
1128
|
+
row['nullable'] == 'Y',
|
1129
|
+
# pass table name for table specific column definitions
|
1130
|
+
table_name,
|
1131
|
+
# pass column type if specified in class definition
|
1132
|
+
get_type_for_column(table_name, oracle_downcase(row['name'])), is_virtual)
|
1133
|
+
end
|
1134
|
+
end
|
1135
|
+
|
1136
|
+
# used just in tests to clear column cache
|
1137
|
+
def clear_columns_cache #:nodoc:
|
1138
|
+
@@columns_cache = nil
|
1139
|
+
@@pk_and_sequence_for_cache = nil
|
1140
|
+
end
|
1141
|
+
|
1142
|
+
# used in migrations to clear column cache for specified table
|
1143
|
+
def clear_table_columns_cache(table_name)
|
1144
|
+
if @@cache_columns
|
1145
|
+
@@columns_cache ||= {}
|
1146
|
+
@@columns_cache[table_name.to_s] = nil
|
1147
|
+
end
|
1148
|
+
end
|
1149
|
+
|
1150
|
+
##
|
1151
|
+
# :singleton-method:
|
1152
|
+
# Specify default sequence start with value (by default 10000 if not explicitly set), e.g.:
|
1153
|
+
#
|
1154
|
+
# ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.default_sequence_start_value = 1
|
1155
|
+
cattr_accessor :default_sequence_start_value
|
1156
|
+
self.default_sequence_start_value = 10000
|
1157
|
+
|
1158
|
+
# Find a table's primary key and sequence.
|
1159
|
+
# *Note*: Only primary key is implemented - sequence will be nil.
|
1160
|
+
def pk_and_sequence_for(table_name, owner=nil, desc_table_name=nil, db_link=nil) #:nodoc:
|
1161
|
+
if @@cache_columns
|
1162
|
+
@@pk_and_sequence_for_cache ||= {}
|
1163
|
+
if @@pk_and_sequence_for_cache.key?(table_name)
|
1164
|
+
@@pk_and_sequence_for_cache[table_name]
|
1165
|
+
else
|
1166
|
+
@@pk_and_sequence_for_cache[table_name] = pk_and_sequence_for_without_cache(table_name, owner, desc_table_name, db_link)
|
1167
|
+
end
|
1168
|
+
else
|
1169
|
+
pk_and_sequence_for_without_cache(table_name, owner, desc_table_name, db_link)
|
1170
|
+
end
|
1171
|
+
end
|
1172
|
+
|
1173
|
+
def pk_and_sequence_for_without_cache(table_name, owner=nil, desc_table_name=nil, db_link=nil) #:nodoc:
|
1174
|
+
(owner, desc_table_name, db_link) = @connection.describe(table_name) unless owner
|
1175
|
+
|
1176
|
+
# changed back from user_constraints to all_constraints for consistency
|
1177
|
+
pks = select_values(<<-SQL.strip.gsub(/\s+/, ' '), 'Primary Key')
|
1178
|
+
SELECT cc.column_name
|
1179
|
+
FROM all_constraints#{db_link} c, all_cons_columns#{db_link} cc
|
1180
|
+
WHERE c.owner = '#{owner}'
|
1181
|
+
AND c.table_name = '#{desc_table_name}'
|
1182
|
+
AND c.constraint_type = 'P'
|
1183
|
+
AND cc.owner = c.owner
|
1184
|
+
AND cc.constraint_name = c.constraint_name
|
1185
|
+
SQL
|
1186
|
+
|
1187
|
+
# only support single column keys
|
1188
|
+
pks.size == 1 ? [oracle_downcase(pks.first), nil] : nil
|
1189
|
+
end
|
1190
|
+
|
1191
|
+
# Returns just a table's primary key
|
1192
|
+
def primary_key(table_name)
|
1193
|
+
pk_and_sequence = pk_and_sequence_for(table_name)
|
1194
|
+
pk_and_sequence && pk_and_sequence.first
|
1195
|
+
end
|
1196
|
+
|
1197
|
+
def has_primary_key?(table_name, owner=nil, desc_table_name=nil, db_link=nil) #:nodoc:
|
1198
|
+
!pk_and_sequence_for(table_name, owner, desc_table_name, db_link).nil?
|
1199
|
+
end
|
1200
|
+
|
1201
|
+
# SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
|
1202
|
+
#
|
1203
|
+
# Oracle requires the ORDER BY columns to be in the SELECT list for DISTINCT
|
1204
|
+
# queries. However, with those columns included in the SELECT DISTINCT list, you
|
1205
|
+
# won't actually get a distinct list of the column you want (presuming the column
|
1206
|
+
# has duplicates with multiple values for the ordered-by columns. So we use the
|
1207
|
+
# FIRST_VALUE function to get a single (first) value for each column, effectively
|
1208
|
+
# making every row the same.
|
1209
|
+
#
|
1210
|
+
# distinct("posts.id", "posts.created_at desc")
|
1211
|
+
def distinct(columns, order_by) #:nodoc:
|
1212
|
+
return "DISTINCT #{columns}" if order_by.blank?
|
1213
|
+
|
1214
|
+
# construct a valid DISTINCT clause, ie. one that includes the ORDER BY columns, using
|
1215
|
+
# FIRST_VALUE such that the inclusion of these columns doesn't invalidate the DISTINCT
|
1216
|
+
order_columns = if order_by.is_a?(String)
|
1217
|
+
order_by.split(',').map { |s| s.strip }.reject(&:blank?)
|
1218
|
+
else # in latest ActiveRecord versions order_by is already Array
|
1219
|
+
order_by
|
1220
|
+
end
|
1221
|
+
order_columns = order_columns.zip((0...order_columns.size).to_a).map do |c, i|
|
1222
|
+
# remove any ASC/DESC modifiers
|
1223
|
+
value = c =~ /^(.+)\s+(ASC|DESC)\s*$/i ? $1 : c
|
1224
|
+
"FIRST_VALUE(#{value}) OVER (PARTITION BY #{columns} ORDER BY #{c}) AS alias_#{i}__"
|
1225
|
+
end
|
1226
|
+
sql = "DISTINCT #{columns}, "
|
1227
|
+
sql << order_columns * ", "
|
1228
|
+
end
|
1229
|
+
|
1230
|
+
def temporary_table?(table_name) #:nodoc:
|
1231
|
+
select_value("SELECT temporary FROM user_tables WHERE table_name = '#{table_name.upcase}'") == 'Y'
|
1232
|
+
end
|
1233
|
+
|
1234
|
+
# ORDER BY clause for the passed order option.
|
1235
|
+
#
|
1236
|
+
# Uses column aliases as defined by #distinct.
|
1237
|
+
#
|
1238
|
+
# In Rails 3.x this method is moved to Arel
|
1239
|
+
def add_order_by_for_association_limiting!(sql, options) #:nodoc:
|
1240
|
+
return sql if options[:order].blank?
|
1241
|
+
|
1242
|
+
order = options[:order].split(',').collect { |s| s.strip }.reject(&:blank?)
|
1243
|
+
order.map! {|s| $1 if s =~ / (.*)/}
|
1244
|
+
order = order.zip((0...order.size).to_a).map { |s,i| "alias_#{i}__ #{s}" }.join(', ')
|
1245
|
+
|
1246
|
+
sql << " ORDER BY #{order}"
|
1247
|
+
end
|
1248
|
+
|
1249
|
+
# construct additional wrapper subquery if select.offset is used to avoid generation of invalid subquery
|
1250
|
+
# ... IN ( SELECT * FROM ( SELECT raw_sql_.*, rownum raw_rnum_ FROM ( ... ) raw_sql_ ) WHERE raw_rnum_ > ... )
|
1251
|
+
def join_to_update(update, select) #:nodoc:
|
1252
|
+
if select.offset
|
1253
|
+
subsubselect = select.clone
|
1254
|
+
subsubselect.projections = [update.key]
|
1255
|
+
|
1256
|
+
subselect = Arel::SelectManager.new(select.engine)
|
1257
|
+
subselect.project Arel.sql(quote_column_name update.key.name)
|
1258
|
+
subselect.from subsubselect.as('alias_join_to_update')
|
1259
|
+
|
1260
|
+
update.where update.key.in(subselect)
|
1261
|
+
else
|
1262
|
+
super
|
1263
|
+
end
|
1264
|
+
end
|
1265
|
+
|
1266
|
+
protected
|
1267
|
+
|
1268
|
+
def translate_exception(exception, message) #:nodoc:
|
1269
|
+
case @connection.error_code(exception)
|
1270
|
+
when 1
|
1271
|
+
RecordNotUnique.new(message, exception)
|
1272
|
+
when 2291
|
1273
|
+
InvalidForeignKey.new(message, exception)
|
1274
|
+
else
|
1275
|
+
super
|
1276
|
+
end
|
1277
|
+
end
|
1278
|
+
|
1279
|
+
private
|
1280
|
+
|
1281
|
+
def select(sql, name = nil, binds = [])
|
1282
|
+
if ActiveRecord.const_defined?(:Result)
|
1283
|
+
exec_query(sql, name, binds).to_a
|
1284
|
+
else
|
1285
|
+
log(sql, name) do
|
1286
|
+
@connection.select(sql, name, false)
|
1287
|
+
end
|
1288
|
+
end
|
1289
|
+
end
|
1290
|
+
|
1291
|
+
def oracle_downcase(column_name)
|
1292
|
+
@connection.oracle_downcase(column_name)
|
1293
|
+
end
|
1294
|
+
|
1295
|
+
def compress_lines(string, join_with = "\n")
|
1296
|
+
string.split($/).map { |line| line.strip }.join(join_with)
|
1297
|
+
end
|
1298
|
+
|
1299
|
+
public
|
1300
|
+
# DBMS_OUTPUT =============================================
|
1301
|
+
#
|
1302
|
+
# PL/SQL in Oracle uses dbms_output for logging print statements
|
1303
|
+
# These methods stick that output into the Rails log so Ruby and PL/SQL
|
1304
|
+
# code can can be debugged together in a single application
|
1305
|
+
|
1306
|
+
# Maximum DBMS_OUTPUT buffer size
|
1307
|
+
DBMS_OUTPUT_BUFFER_SIZE = 10000 # can be 1-1000000
|
1308
|
+
|
1309
|
+
# Turn DBMS_Output logging on
|
1310
|
+
def enable_dbms_output
|
1311
|
+
set_dbms_output_plsql_connection
|
1312
|
+
@enable_dbms_output = true
|
1313
|
+
plsql(:dbms_output).sys.dbms_output.enable(DBMS_OUTPUT_BUFFER_SIZE)
|
1314
|
+
end
|
1315
|
+
# Turn DBMS_Output logging off
|
1316
|
+
def disable_dbms_output
|
1317
|
+
set_dbms_output_plsql_connection
|
1318
|
+
@enable_dbms_output = false
|
1319
|
+
plsql(:dbms_output).sys.dbms_output.disable
|
1320
|
+
end
|
1321
|
+
# Is DBMS_Output logging enabled?
|
1322
|
+
def dbms_output_enabled?
|
1323
|
+
@enable_dbms_output
|
1324
|
+
end
|
1325
|
+
|
1326
|
+
protected
|
1327
|
+
def log(sql, name, binds = nil) #:nodoc:
|
1328
|
+
if binds
|
1329
|
+
super sql, name, binds
|
1330
|
+
else
|
1331
|
+
super sql, name
|
1332
|
+
end
|
1333
|
+
ensure
|
1334
|
+
log_dbms_output if dbms_output_enabled?
|
1335
|
+
end
|
1336
|
+
|
1337
|
+
private
|
1338
|
+
|
1339
|
+
def set_dbms_output_plsql_connection
|
1340
|
+
raise OracleEnhancedConnectionException, "ruby-plsql gem is required for logging DBMS output" unless self.respond_to?(:plsql)
|
1341
|
+
# do not reset plsql connection if it is the same (as resetting will clear PL/SQL metadata cache)
|
1342
|
+
unless plsql(:dbms_output).connection && plsql(:dbms_output).connection.raw_connection == raw_connection
|
1343
|
+
plsql(:dbms_output).connection = raw_connection
|
1344
|
+
end
|
1345
|
+
end
|
1346
|
+
|
1347
|
+
def log_dbms_output
|
1348
|
+
while true do
|
1349
|
+
result = plsql(:dbms_output).sys.dbms_output.get_line(:line => '', :status => 0)
|
1350
|
+
break unless result[:status] == 0
|
1351
|
+
@logger.debug "DBMS_OUTPUT: #{result[:line]}" if @logger
|
1352
|
+
end
|
1353
|
+
end
|
1354
|
+
|
1355
|
+
end
|
1356
|
+
end
|
1357
|
+
end
|
1358
|
+
|
1359
|
+
# Added LOB writing callback for sessions stored in database
|
1360
|
+
# Otherwise it is not working as Session class is defined before OracleAdapter is loaded in Rails 2.0
|
1361
|
+
if defined?(CGI::Session::ActiveRecordStore::Session)
|
1362
|
+
if !CGI::Session::ActiveRecordStore::Session.respond_to?(:after_save_callback_chain) ||
|
1363
|
+
CGI::Session::ActiveRecordStore::Session.after_save_callback_chain.detect{|cb| cb.method == :enhanced_write_lobs}.nil?
|
1364
|
+
#:stopdoc:
|
1365
|
+
class CGI::Session::ActiveRecordStore::Session
|
1366
|
+
after_save :enhanced_write_lobs
|
1367
|
+
end
|
1368
|
+
#:startdoc:
|
1369
|
+
end
|
1370
|
+
end
|
1371
|
+
|
1372
|
+
# Implementation of standard schema definition statements and extensions for schema definition
|
1373
|
+
require 'active_record/connection_adapters/oracle_enhanced_schema_statements'
|
1374
|
+
require 'active_record/connection_adapters/oracle_enhanced_schema_statements_ext'
|
1375
|
+
|
1376
|
+
# Extensions for schema definition
|
1377
|
+
require 'active_record/connection_adapters/oracle_enhanced_schema_definitions'
|
1378
|
+
|
1379
|
+
# Extensions for context index definition
|
1380
|
+
require 'active_record/connection_adapters/oracle_enhanced_context_index'
|
1381
|
+
|
1382
|
+
# Load custom create, update, delete methods functionality
|
1383
|
+
require 'active_record/connection_adapters/oracle_enhanced_procedures'
|
1384
|
+
|
1385
|
+
# Load additional methods for composite_primary_keys support
|
1386
|
+
require 'active_record/connection_adapters/oracle_enhanced_cpk'
|
1387
|
+
|
1388
|
+
# Load patch for dirty tracking methods
|
1389
|
+
require 'active_record/connection_adapters/oracle_enhanced_dirty'
|
1390
|
+
|
1391
|
+
# Load rake tasks definitions
|
1392
|
+
begin
|
1393
|
+
require 'active_record/connection_adapters/oracle_enhanced_tasks'
|
1394
|
+
rescue LoadError
|
1395
|
+
end if defined?(Rails) || defined?(RAILS_ROOT)
|
1396
|
+
|
1397
|
+
# Patches and enhancements for schema dumper
|
1398
|
+
require 'active_record/connection_adapters/oracle_enhanced_schema_dumper'
|
1399
|
+
|
1400
|
+
# Implementation of structure dump
|
1401
|
+
require 'active_record/connection_adapters/oracle_enhanced_structure_dump'
|
1402
|
+
|
1403
|
+
# Add BigDecimal#to_d, Fixnum#to_d and Bignum#to_d methods if not already present
|
1404
|
+
require 'active_record/connection_adapters/oracle_enhanced_core_ext'
|
1405
|
+
|
1406
|
+
require 'active_record/connection_adapters/oracle_enhanced_activerecord_patches'
|
1407
|
+
|
1408
|
+
require 'active_record/connection_adapters/oracle_enhanced_version'
|