Permalink
Browse files

Added an Oracle adapter that works with the Oracle bindings by Yoshida (

http://raa.ruby-lang.org/project/oracle/) #564 [Maik Schmidt]

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@522 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information...
1 parent 8382e6e commit 336c2cbb8f728825139f2ab94b96726935ea18a1 @dhh dhh committed Feb 7, 2005
@@ -1,5 +1,20 @@
*SVN*
+* Added an Oracle adapter that works with the Oracle bindings by Yoshida (http://raa.ruby-lang.org/project/oracle/) #564 [Maik Schmidt]
+
+ A number of caveats:
+
+ 1. You have to create a sequence for every table that need an auto-generated primary key.
+ The sequence name is built by appending "_id" to the according table_name,
+ i.e. if you have a table called "employees", you have to create a sequence called "employees_id".
+
+ 2. The data type TIME is not supported, because Oracle does not support it.
+
+ 3. Binary data (BLOBs etc.) are currently not supported. Will be fixed soon.
+
+ 4. LIMIT clauses with OFFSET are not supported, because I could not find a way to simulate this behaviour using ROWNUM and "SELECT *".
+
+
* Fixed Base#clone for use with PostgreSQL #565 [hanson@surgery.wisc.edu]
@@ -63,6 +63,12 @@ Rake::TestTask.new("test_db2") { |t|
t.verbose = true
}
+Rake::TestTask.new("test_oracle") { |t|
+ t.libs << "test" << "test/connections/native_oracle"
+ t.pattern = 'test/*_test.rb'
+ t.verbose = true
+}
+
# Generate the RDoc documentation
Rake::RDocTask.new { |rdoc|
@@ -37,6 +37,7 @@
active_record/connection_adapters/abstract_adapter.rb
active_record/connection_adapters/db2_adapter.rb
active_record/connection_adapters/mysql_adapter.rb
+ active_record/connection_adapters/oracle_adapter.rb
active_record/connection_adapters/postgresql_adapter.rb
active_record/connection_adapters/sqlite_adapter.rb
active_record/connection_adapters/sqlserver_adapter.rb
@@ -65,6 +66,7 @@
active_record/timestamp.rb
active_record/transactions.rb
active_record/validations.rb
+ active_record/vendor/db2.rb
active_record/vendor/mysql.rb
active_record/vendor/mysql411.rb
active_record/vendor/simple.rb
@@ -60,3 +60,4 @@
require 'active_record/connection_adapters/sqlite_adapter'
require 'active_record/connection_adapters/sqlserver_adapter'
require 'active_record/connection_adapters/db2_adapter'
+require 'active_record/connection_adapters/oracle_adapter'
@@ -1,5 +1,4 @@
-# db2_adapter.rb
-# author: Maik Schmidt <contact@maik-schmidt.de>
+# Author: Maik Schmidt <contact@maik-schmidt.de>
require 'active_record/connection_adapters/abstract_adapter'
@@ -100,30 +99,31 @@ def columns(table_name, name = nil)
end
private
- def last_insert_id
- row = select_one(<<-GETID.strip)
- with temp(id) as (values (identity_val_local())) select * from temp
- GETID
- row['id'].to_i
+
+ def last_insert_id
+ row = select_one(<<-GETID.strip)
+ with temp(id) as (values (identity_val_local())) select * from temp
+ GETID
+ row['id'].to_i
+ end
+
+ def select(sql, name = nil)
+ stmt = nil
+ log(sql, name, @connection) do |connection|
+ stmt = DB2::Statement.new(connection)
+ stmt.exec_direct(sql + " with ur")
end
- def select(sql, name = nil)
- stmt = nil
- log(sql, name, @connection) do |connection|
- stmt = DB2::Statement.new(connection)
- stmt.exec_direct(sql + " with ur")
- end
-
- rows = []
- while row = stmt.fetch_as_hash
- rows << row
- end
- stmt.free
- rows
+ rows = []
+ while row = stmt.fetch_as_hash
+ rows << row
end
+ stmt.free
+ rows
+ end
end
end
end
rescue LoadError
# DB2 driver is unavailable.
-end
+end
@@ -0,0 +1,244 @@
+# Author: Maik Schmidt <contact@maik-schmidt.de>
+require 'active_record/connection_adapters/abstract_adapter'
+require 'date'
+
+begin
+ require 'oracle' unless self.class.const_defined?(:ORAconn)
+
+ module ActiveRecord
+ class Base
+ # Establishes a connection to the database that's used by
+ # all Active Record objects
+ def self.oracle_connection(config) # :nodoc:
+ symbolize_strings_in_hash(config)
+ usr = config[:username] || ''
+ pwd = config[:password] || ''
+ db = config[:database] || ''
+
+ connection = ORAconn.logon(usr, pwd, db)
+ cursor = connection.open
+ cursor.parse("ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD'")
+ rows_affected = cursor.exec
+ cursor.close
+ ConnectionAdapters::OracleAdapter.new(connection)
+ end
+ end
+
+ module ConnectionAdapters
+ class OracleAdapter < AbstractAdapter
+ def select_all(sql, name = nil)
+ select(sql, name)
+ end
+
+ def select_one(sql, name = nil)
+ select(sql, name).first
+ end
+
+ # Oracle does not support auto-generated columns, so we have to use
+ # sequences. Every insert is followed by a select statement, which
+ # returns the current sequence value. The statements are encapsulated
+ # in an anonymous PL/SQL block (supported since Oracle 7.3) to prevent
+ # race conditions and to maximize performance.
+ def insert(sql, name = nil, pk = nil, id_value = nil)
+ new_id = nil
+ if !id_value.nil?
+ execute(sql, name)
+ new_id = id_value
+ else
+ pk_col = pk || ActiveRecord::Base::primary_key
+ if sql !~ Regexp.new('\b' + pk_col + '\b')
+ seq_name = sql.sub(/^\s*insert\s+?into\s+(\S+).*$/im, '\1') + "_id"
+ sql.sub!(/(into\s+.*?\()/im, '\1' + "#{pk_col}, ")
+ sql.sub!(/(values\s*\()/im, '\1' + "#{seq_name}.nextval, ")
+ new_id = ' ' * 40 # Enough space for String representation of ID?
+ log(sql, name, @connection) do |connection|
+ cursor = connection.open
+ s = "begin #{sql}; select #{seq_name}.currval into :new_id from dual; end;"
+ cursor.parse(s)
+ cursor.bindrv(':new_id', new_id)
+ cursor.exec
+ cursor.close
+ new_id = new_id.to_i
+ end
+ else
+ execute(sql, name)
+ end
+ end
+ new_id
+ end
+
+ def execute(sql, name = nil)
+ rows_affected = 0
+ log(sql, name, @connection) do |connection|
+ cursor = connection.open
+ cursor.parse(sql)
+ rows_affected = cursor.exec
+ cursor.close
+ end
+ rows_affected
+ end
+
+ alias_method :update, :execute
+ alias_method :delete, :execute
+
+ def begin_db_transaction
+ @connection.commitoff
+ end
+
+ def commit_db_transaction
+ @connection.commit
+ @connection.commiton
+ end
+
+ def rollback_db_transaction
+ @connection.rollback
+ @connection.commiton
+ end
+
+ def quote_column_name(name) name; end
+
+ def quote(value, column = nil)
+ if column && column.type == :timestamp && value.class.to_s == "String"
+ begin
+ value = DateTime.parse(value)
+ rescue => ex
+ # Value cannot be parsed.
+ end
+ end
+
+ case value
+ when String
+ if column && column.type == :binary
+ "'#{quote_string(column.string_to_binary(value))}'" # ' (for ruby-mode)
+ else
+ "'#{quote_string(value)}'" # ' (for ruby-mode)
+ end
+ when NilClass then "NULL"
+ when TrueClass then (column && column.type == :boolean ? "'t'" : "1")
+ when FalseClass then (column && column.type == :boolean ? "'f'" : "0")
+ when Float, Fixnum, Bignum then value.to_s
+ when Date then "to_date('#{value.strftime("%Y-%m-%d")}', 'YYYY-MM-DD')"
+ when Time, DateTime then "to_date('#{value.strftime("%Y-%m-%d %H:%M:%S")}', 'YYYY-MM-DD HH24:MI:SS')"
+ else "'#{quote_string(value.to_yaml)}'"
+ end
+ end
+
+ def quote_string(s)
+ s.gsub(/'/, "''") # ' (for ruby-mode)
+ end
+
+ def add_limit!(sql, limit)
+ l, o = limit.to_s.scan(/\d+/)
+ if o.nil?
+ sql.sub!(/^.*$/im, "select * from (#{sql}) where rownum <= #{l}")
+ else
+ raise ArgumentError, "LIMIT clauses with OFFSET are not supported yet!"
+ end
+ end
+
+ def columns(table_name, name = nil)
+ sql = <<-SQL
+ select column_name,
+ data_type,
+ data_length,
+ data_precision,
+ data_default
+ from user_tab_columns
+ where table_name = upper('#{table_name}')
+ SQL
+ result = []
+ cols = select_all(sql, name)
+ cols.each do |c|
+ name = c['column_name'].downcase
+ default = c['data_default']
+ default = (default == 'NULL') ? nil : default
+ type = get_sql_type(c['data_type'], c['data_length'])
+ result << Column.new(name, default, type)
+ end
+ result
+ end
+
+ private
+
+ def get_sql_type(type_name, type_length)
+ case type_name
+ when /timestamp/i
+ return "TIMESTAMP"
+ when /number/i
+ return "INT"
+ when /date/i
+ return "DATE"
+ else
+ return "#{type_name}(#{type_length})"
+ end
+ end
+
+ def select(sql, name = nil)
+ col_names = []
+ cursor = nil
+ log(sql, name, @connection) do |connection|
+ cursor = connection.open
+ col_names = parse(cursor, sql)
+ cursor.exec
+ end
+
+ rows = []
+ while cursor.fetch do
+ row = {}
+ col_names.each_with_index do |name, i|
+ row[name] = cursor.getCol(i + 1)[0]
+ end
+ rows << row
+ end
+ cursor.close
+ rows
+ end
+
+ VARCHAR2 = 1
+ NUMBER = 2
+ INTEGER = 3 ## external
+ FLOAT = 4 ## external
+ LONG = 8
+ ROWID = 11
+ DATE = 12
+ RAW = 23
+ LONG_RAW = 24
+ UNSIGNED_INT = 68 ## external
+ CHAR = 96
+ MLSLABEL = 105
+
+ def parse(cursor, sql)
+ cursor.parse(sql)
+ colnr = 1
+ col_names = []
+ loop {
+ colinfo = cursor.describe(colnr)
+ break if colinfo.nil?
+
+ col_names << colinfo[2].downcase
+ collength, coltype = colinfo[3], colinfo[1]
+
+ collength, coltype = case coltype
+ when NUMBER
+ [40, VARCHAR2]
+ when VARCHAR2, CHAR
+ [(collength * 1.5).ceil, VARCHAR2]
+ when LONG
+ [65535, LONG]
+ when LONG_RAW
+ [65535, LONG_RAW]
+ else
+ [collength, VARCHAR2]
+ end
+
+ cursor.define(colnr, collength, coltype)
+ colnr += 1
+ }
+ col_names
+ end
+ end
+ end
+ end
+rescue LoadError
+ # Oracle driver is unavailable.
+end
@@ -176,6 +176,10 @@ def test_preserving_date_objects
"The last_read attribute should be of the Date class"
)
+ # Oracle does not have a TIME datatype.
+ if ActiveRecord::ConnectionAdapters.const_defined? :OracleAdapter
+ return true if ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::OracleAdapter)
+ end
assert_kind_of(
Time, Topic.find(1).bonus_time,
"The bonus_time attribute should be of the Time class"
@@ -381,6 +385,11 @@ def test_default_values
end
def test_utc_as_time_zone
+ # Oracle does not have a TIME datatype.
+ if ActiveRecord::ConnectionAdapters.const_defined? :OracleAdapter
+ return true if ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::OracleAdapter)
+ end
+
Topic.default_timezone = :utc
attributes = { "bonus_time" => "5:42:00AM" }
topic = Topic.find(1)
@@ -506,6 +515,11 @@ def test_multiparameter_attributes_on_time_with_empty_seconds
end
def test_attributes_on_dummy_time
+ # Oracle does not have a TIME datatype.
+ if ActiveRecord::ConnectionAdapters.const_defined? :OracleAdapter
+ return true if ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::OracleAdapter)
+ end
+
attributes = {
"bonus_time" => "5:42:00AM"
}
Oops, something went wrong.

0 comments on commit 336c2cb

Please sign in to comment.