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
This commit is contained in:
David Heinemeier Hansson 2005-02-07 14:06:00 +00:00
parent 8382e6e506
commit 336c2cbb8f
18 changed files with 558 additions and 32 deletions

View File

@ -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]

View File

@ -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|

View File

@ -37,6 +37,7 @@ files = %w-
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 @@ files = %w-
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

View File

@ -60,3 +60,4 @@ require 'active_record/connection_adapters/postgresql_adapter'
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'

View File

@ -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 @@ begin
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

View File

@ -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

View File

@ -176,6 +176,10 @@ class BasicsTest < Test::Unit::TestCase
"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 @@ class BasicsTest < Test::Unit::TestCase
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 @@ class BasicsTest < Test::Unit::TestCase
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"
}

View File

@ -13,6 +13,10 @@ class BinaryTest < Test::Unit::TestCase
if ActiveRecord::ConnectionAdapters.const_defined? :DB2Adapter
return true if ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::DB2Adapter)
end
if ActiveRecord::ConnectionAdapters.const_defined? :OracleAdapter
return true if ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::OracleAdapter)
end
bin = Binary.new
bin.data = @data
@ -27,10 +31,10 @@ class BinaryTest < Test::Unit::TestCase
db_bin = Binary.find(bin.id)
assert db_bin.data == bin.data,
"Loaded binary data differes from memory version"
"Loaded binary data differs from memory version"
assert db_bin.data == File.new(File.dirname(__FILE__)+"/fixtures/associations.png","rb").read,
"Loaded binary data differes from file version"
"Loaded binary data differs from file version"
end
private

View File

@ -5,8 +5,15 @@ class TestColumnAlias < Test::Unit::TestCase
def test_column_alias
topic = Topic.find(1)
records = topic.connection.select_all("SELECT id AS pk FROM topics LIMIT 1")
assert_equal(records[0].keys[0], "pk")
if ActiveRecord::ConnectionAdapters.const_defined? :OracleAdapter
if ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::OracleAdapter)
records = topic.connection.select_all("SELECT id AS pk FROM topics WHERE ROWNUM < 2")
assert_equal(records[0].keys[0], "pk")
end
else
records = topic.connection.select_all("SELECT id AS pk FROM topics LIMIT 1")
assert_equal(records[0].keys[0], "pk")
end
end
end

View File

@ -0,0 +1,24 @@
print "Using native Oracle\n"
require 'fixtures/course'
require 'logger'
ActiveRecord::Base.logger = Logger.new("debug.log")
db1 = 'local'
db2 = 'local'
ActiveRecord::Base.establish_connection(
:adapter => "oracle",
:host => "localhost",
:username => "arunit",
:password => "arunit",
:database => db1
)
Course.establish_connection(
:adapter => "oracle",
:host => "localhost",
:username => "arunit2",
:password => "arunit2",
:database => db2
)

View File

@ -34,10 +34,16 @@ class FinderTest < Test::Unit::TestCase
end
def test_find_all_with_prepared_limit_and_offset
entrants = Entrant.find_all nil, "id ASC", ["? OFFSET ?", 2, 1]
assert_equal(2, entrants.size)
assert_equal(@entrants["second"]["name"], entrants.first.name)
if ActiveRecord::ConnectionAdapters.const_defined? :OracleAdapter
if ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::OracleAdapter)
assert_raises(ArgumentError) { Entrant.find_all nil, "id ASC", ["? OFFSET ?", 2, 1] }
end
else
entrants = Entrant.find_all nil, "id ASC", ["? OFFSET ?", 2, 1]
assert_equal(2, entrants.size)
assert_equal(@entrants["second"]["name"], entrants.first.name)
end
end
def test_find_with_entire_select_statement

View File

@ -0,0 +1,5 @@
sqlplus arunit/arunit @ drop_oracle_tables
sqlplus arunit/arunit @ oracle
sqlplus arunit2/arunit2 @ drop_oracle_tables2
sqlplus arunit2/arunit2 @ oracle2

View File

@ -0,0 +1,5 @@
sqlplus arunit/arunit @ drop_oracle_tables
sqlplus arunit/arunit @ oracle
sqlplus arunit2/arunit2 @ drop_oracle_tables2
sqlplus arunit2/arunit2 @ oracle2

View File

@ -0,0 +1,35 @@
DROP TABLE accounts;
DROP SEQUENCE accounts_id;
DROP TABLE companies;
DROP SEQUENCE companies_id;
DROP TABLE topics;
DROP SEQUENCE topics_id;
DROP TABLE developers;
DROP SEQUENCE developers_id;
DROP TABLE projects;
DROP SEQUENCE projects_id;
DROP TABLE developers_projects;
DROP SEQUENCE developers_projects_id;
DROP TABLE customers;
DROP SEQUENCE customers_id;
DROP TABLE movies;
DROP SEQUENCE movies_id;
DROP TABLE subscribers;
DROP SEQUENCE subscribers_id;
DROP TABLE booleantests;
DROP SEQUENCE booleantests_id;
DROP TABLE auto_id_tests;
DROP SEQUENCE auto_id_tests_id;
DROP TABLE entrants;
DROP SEQUENCE entrants_id;
DROP TABLE colnametests;
DROP SEQUENCE colnametests_id;
DROP TABLE mixins;
DROP SEQUENCE mixins_id;
DROP TABLE people;
DROP SEQUENCE people_id;
DROP TABLE binaries;
DROP SEQUENCE binaries_id;
DROP TABLE computers;
DROP SEQUENCE computers_id;
EXIT;

View File

@ -0,0 +1,3 @@
DROP TABLE courses;
DROP SEQUENCE courses_id;
EXIT;

View File

@ -0,0 +1,149 @@
CREATE TABLE accounts (
id int NOT NULL,
firm_id int default NULL,
credit_limit int default NULL,
PRIMARY KEY (id)
);
CREATE SEQUENCE accounts_id MINVALUE 10000;
CREATE TABLE companies (
id int NOT NULL,
type varchar(50) default NULL,
ruby_type varchar(50) default NULL,
firm_id int default NULL,
name varchar(50) default NULL,
client_of int default NULL,
rating int default 1,
PRIMARY KEY (id)
);
CREATE SEQUENCE companies_id MINVALUE 10000;
CREATE TABLE topics (
id int NOT NULL,
title varchar(255) default NULL,
author_name varchar(255) default NULL,
author_email_address varchar(255) default NULL,
written_on timestamp default NULL,
bonus_time timestamp default NULL,
last_read date default NULL,
content varchar(3000),
approved smallint default 1,
replies_count int default 0,
parent_id int default NULL,
type varchar(50) default NULL,
PRIMARY KEY (id)
);
CREATE SEQUENCE topics_id MINVALUE 10000;
CREATE TABLE developers (
id int NOT NULL,
name varchar(100) default NULL,
salary int default 70000,
PRIMARY KEY (id)
);
CREATE SEQUENCE developers_id MINVALUE 10000;
CREATE TABLE projects (
id int NOT NULL,
name varchar(100) default NULL,
PRIMARY KEY (id)
);
CREATE SEQUENCE projects_id MINVALUE 10000;
CREATE TABLE developers_projects (
developer_id int NOT NULL,
project_id int NOT NULL,
joined_on date default NULL
);
CREATE SEQUENCE developers_projects_id MINVALUE 10000;
CREATE TABLE customers (
id int NOT NULL,
name varchar(100) default NULL,
balance int default 0,
address_street varchar(100) default NULL,
address_city varchar(100) default NULL,
address_country varchar(100) default NULL,
PRIMARY KEY (id)
);
CREATE SEQUENCE customers_id MINVALUE 10000;
CREATE TABLE movies (
movieid int NOT NULL,
name varchar(100) default NULL,
PRIMARY KEY (movieid)
);
CREATE SEQUENCE movies_id MINVALUE 10000;
CREATE TABLE subscribers (
nick varchar(100) NOT NULL,
name varchar(100) default NULL,
PRIMARY KEY (nick)
);
CREATE SEQUENCE subscribers_id MINVALUE 10000;
CREATE TABLE booleantests (
id int NOT NULL,
value int default NULL,
PRIMARY KEY (id)
);
CREATE SEQUENCE booleantests_id MINVALUE 10000;
CREATE TABLE auto_id_tests (
auto_id int NOT NULL,
value int default NULL,
PRIMARY KEY (auto_id)
);
CREATE SEQUENCE auto_id_tests_id MINVALUE 10000;
CREATE TABLE entrants (
id int NOT NULL PRIMARY KEY,
name varchar(255) NOT NULL,
course_id int NOT NULL
);
CREATE SEQUENCE entrants_id MINVALUE 10000;
CREATE TABLE colnametests (
id int NOT NULL,
references int NOT NULL,
PRIMARY KEY (id)
);
CREATE SEQUENCE colnametests_id MINVALUE 10000;
CREATE TABLE mixins (
id int NOT NULL,
parent_id int default NULL,
pos int default NULL,
created_at timestamp default NULL,
updated_at timestamp default NULL,
lft int default NULL,
rgt int default NULL,
root_id int default NULL,
type varchar(40) default NULL,
PRIMARY KEY (id)
);
CREATE SEQUENCE mixins_id MINVALUE 10000;
CREATE TABLE people (
id int NOT NULL,
first_name varchar(40) NOT NULL,
lock_version int default 0,
PRIMARY KEY (id)
);
CREATE SEQUENCE people_id MINVALUE 10000;
CREATE TABLE binaries (
id int NOT NULL,
data blob,
PRIMARY KEY (id)
);
CREATE SEQUENCE binaries_id MINVALUE 10000;
CREATE TABLE computers (
id int,
developer int NOT NULL,
PRIMARY KEY (id)
);
CREATE SEQUENCE computers_id MINVALUE 10000;
EXIT;

View File

@ -0,0 +1,7 @@
CREATE TABLE courses (
id int NOT NULL PRIMARY KEY,
name varchar(255) NOT NULL
);
CREATE SEQUENCE courses_id MINVALUE 10000;
EXIT;

View File

@ -3,8 +3,7 @@ first:
title: The First Topic
author_name: David
author_email_address: david@loudthinking.com
written_on: 2003-07-16 15:28:00
bonus_time: '12:13:14'
written_on: 2003-07-16t15:28:00.00+01:00
last_read: 2004-04-15
content: Have a nice day
approved: 0
@ -14,7 +13,7 @@ second:
id: 2
title: The Second Topic's of the day
author_name: Mary
written_on: 2003-07-15 15:28:00
written_on: 2003-07-15t15:28:00.00+01:00
content: Have a nice day
approved: 1
replies_count: 2