ruby-plsql 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,10 @@
1
+ .svn
2
+ .DS_Store
3
+ coverage
4
+ doc
5
+ pkg
6
+ log
7
+ tmp
8
+ sqlnet.log
9
+ ruby-plsql.gemspec
10
+
@@ -1,3 +1,10 @@
1
+ == 0.4.0 2009-11-23
2
+
3
+ * New features
4
+ * Support for PL/SQL RECORD, BOOLEAN, TABLE, VARRAY, OBJECT and CURSOR parameters and return values
5
+ * Support for basic table and sequence operations
6
+ * A lot of refactoring
7
+
1
8
  == 0.3.1 2009-06-05
2
9
 
3
10
  * Bug fixes
@@ -1,58 +1,126 @@
1
1
  = ruby-plsql
2
2
 
3
- * http://rubyforge.org/projects/ruby-plsql/
3
+ Ruby API for calling Oracle PL/SQL procedures.
4
4
 
5
- == DESCRIPTION:
5
+ == DESCRIPTION
6
6
 
7
- ruby-plsql gem provides simple Ruby API for calling Oracle PL/SQL procedures.
8
- ruby-plsql support both Ruby 1.8 MRI, Ruby 1.9.1 YARV and JRuby runtime environments.
9
- This gem requires ruby-oci8 library version 1.x (if MRI is used) or 2.x (if Ruby 1.9.1 is used) or Oracle JDBC driver (ojdbc14.jar) (if JRuby is used) for connection to Oracle database.
7
+ ruby-plsql gem provides simple Ruby API for calling Oracle PL/SQL procedures. It could be used both for accessing Oracle PL/SQL API procedures in legacy applications as well as it could be used to create PL/SQL unit tests using Ruby testing libraries.
10
8
 
11
- See http://blog.rayapps.com for more information.
9
+ NUMBER, VARCHAR2, DATE, TIMESTAMP, CLOB, BLOB, BOOLEAN, PL/SQL RECORD, TABLE, VARRAY, OBJECT and CURSOR types are supported for input and output parameters and return values of PL/SQL procedures and functions.
12
10
 
13
- Look ar RSpec tests under spec directory for usage examples.
11
+ ruby-plsql support both Ruby 1.8 MRI, Ruby 1.9.1 YARV and JRuby 1.3/1.4 runtime environments.
14
12
 
15
- == FEATURES/PROBLEMS:
13
+ == USAGE
16
14
 
17
- * Currently just NUMBER, VARCHAR2, DATE, TIMESTAMP, CLOB, BLOB argument types are supported for PL/SQL procedures
15
+ === Calling PL/SQL functions and procedures:
18
16
 
19
- == SYNOPSIS:
17
+ require "rubygems"
18
+ require "ruby-plsql"
20
19
 
21
- Usage examples:
20
+ plsql.connection = OCI8.new("hr","hr","xe")
22
21
 
23
- require "ruby_plsql"
22
+ plsql.test_uppercase('xxx') # => "XXX"
23
+ plsql.test_uppercase(:p_string => 'xxx') # => "XXX"
24
+ plsql.test_copy("abc", nil, nil) # => { :p_to => "abc", :p_to_double => "abcabc" }
25
+ plsql.test_copy(:p_from => "abc", :p_to => nil, :p_to_double => nil)
26
+ # => { :p_to => "abc", :p_to_double => "abcabc" }
27
+ plsql.hr.test_uppercase('xxx') # => "XXX"
28
+ plsql.test_package.test_uppercase('xxx') # => 'XXX'
24
29
 
25
- plsql.connection = OCI8.new("hr","hr","xe")
30
+ # PL/SQL records or object type parameters should be passed as Hash
31
+ p_employee = { :employee_id => 1, :first_name => 'First', :last_name => 'Last', :hire_date => Time.local(2000,01,31) }
32
+ plsql.test_full_name(p_employee)
26
33
 
27
- plsql.test_uppercase('xxx') # => "XXX"
28
- plsql.test_uppercase(:p_string => 'xxx') # => "XXX"
29
- plsql.test_copy("abc", nil, nil) # => { :p_to => "abc", :p_to_double => "abcabc" }
30
- plsql.test_copy(:p_from => "abc", :p_to => nil, :p_to_double => nil)
31
- # => { :p_to => "abc", :p_to_double => "abcabc" }
32
- plsql.hr.test_uppercase('xxx') # => "XXX"
33
- plsql.test_package.test_uppercase('xxx') # => 'XXX'
34
+ # TABLE or VARRAY parameters should be passed as Array
35
+ plsql.test_sum([1,2,3,4])
34
36
 
35
- plsql.logoff
37
+ # Nested objects or arrays are also supported
38
+ p_employee = { :employee_id => 1, :first_name => 'First', :last_name => 'Last', :hire_date => Time.local(2000,01,31),
39
+ :address => {:street => 'Street', :city => 'City', :country => 'Country},
40
+ :phones => [{:type => 'mobile', :phone_number => '123456'}, {:type => 'fixed', :phone_number => '654321'}]}
41
+ plsql.test_store_employee(p_employee)
36
42
 
43
+ # Returned cursor can be fetched
44
+ plsql.test_cursor do |cursor|
45
+ cursor.fetch # => one row from cursor
46
+ cursor.fetch_all # => all rows from cursor
47
+ end
48
+
49
+ plsql.connection.autocommit = false
50
+ plsql.commit
51
+ plsql.rollback
52
+
53
+ plsql.logoff
54
+
55
+ Look at RSpec tests under spec directory for more usage examples.
56
+
57
+
58
+ === Table operations:
59
+
60
+ ruby-plsql also provides simple API for select/insert/update/delete table operations (with Sequel-like syntax). This could be useful if ruby-plsql is used without ActiveRecord (e.g. for writing PL/SQL unit tests):
61
+
62
+ # insert record in table
63
+ employee = { :employee_id => 1, :first_name => 'First', :last_name => 'Last', :hire_date => Time.local(2000,01,31) }
64
+ plsql.employees.insert employee # INSERT INTO employees VALUES (1, 'First', 'Last', ...)
65
+
66
+ # insert many records
67
+ employees = [employee1, employee2, ... ] # array of many Hashes
68
+ plsql.employees.insert employees
69
+
70
+ # select one record
71
+ plsql.employees.first # SELECT * FROM employees
72
+ # fetch first row => {:employee_id => ..., :first_name => '...', ...}
73
+ plsql.employees.first(:employee_id => 1) # SELECT * FROM employees WHERE employee_id = 1
74
+ plsql.employees.first("WHERE employee_id = 1")
75
+ plsql.employees.first("WHERE employee_id = :employee_id", 1)
76
+
77
+ # select many records
78
+ plsql.employees.all # => [{...}, {...}, ...]
79
+ plsql.employees.all(:order_by => :employee_id)
80
+ plsql.employees.all("WHERE employee_id > :employee_id", 5)
81
+
82
+ # count records
83
+ plsql.employees.count # SELECT COUNT(*) FROM employees
84
+ plsql.employees.count("WHERE employee_id > :employee_id", 5)
85
+
86
+ # update records
87
+ plsql.employees.update(:first_name => 'Second', :where => {:employee_id => 1})
88
+ # UPDATE employees SET first_name = 'Second' WHERE employee_id = 1
89
+
90
+ # delete records
91
+ plsql.employees.delete(:employee_id => 1) # DELETE FROM employees WHERE employee_id = 1
92
+
93
+ # select from sequences
94
+ plsql.employees_seq.nextval # SELECT employees_seq.NEXTVAL FROM dual
95
+ plsql.employees_seq.currval # SELECT employees_seq.CURRVAL FROM dual
96
+
97
+
98
+ === Usage with Rails:
37
99
 
38
100
  If using with Rails then include in initializer file:
39
101
 
40
- plsql.activerecord_class = ActiveRecord::Base
102
+ plsql.activerecord_class = ActiveRecord::Base
41
103
 
42
104
  and then you do not need to specify plsql.connection (this is also safer when ActiveRecord reestablishes connection to database).
43
105
 
44
106
  == REQUIREMENTS:
45
107
 
46
- Ruby 1.8 MRI
47
- * Requires ruby-oci8 library to connect to Oracle (please use version 1.0.3 or later)
48
- Ruby 1.9.1
49
- * Requires ruby-oci8 library to connect to Oracle (please use version 2.0 or later)
50
- JRuby
51
- * Requires Oracle JDBC driver (ojdbc14.jar should be somewhere in PATH) to connect to Oracle
108
+ Ruby 1.8.6/1.8.7 MRI / Ruby 1.9.1 YARV
109
+ * Requires ruby-oci8 library (please use version 2.0.3 or later)
110
+ JRuby 1.3/1.4
111
+ * Requires Oracle JDBC driver (ojdbc14.jar should be somewhere in PATH or should be available for Java class loader)
52
112
 
53
113
  == INSTALL:
54
114
 
55
- * sudo gem install ruby-plsql
115
+ * [sudo] gem install ruby-plsql
116
+
117
+ In addition install either ruby-oci8 (for MRI/YARV) or copy Oracle JDBC driver to $JRUBY_HOME/lib (for JRuby).
118
+
119
+ == LINKS
120
+
121
+ * Source code: http://github.com/rsim/ruby-plsql
122
+ * Bug reports / Feature requests: http://github.com/rsim/ruby-plsql/issues
123
+ * Discuss at oracle_enhanced adapter group: http://groups.google.com/group/oracle-enhanced
56
124
 
57
125
  == CONTRIBUTORS:
58
126
 
@@ -0,0 +1,49 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "ruby-plsql"
8
+ gem.summary = "Ruby API for calling Oracle PL/SQL procedures."
9
+ gem.description = <<-EOS
10
+ ruby-plsql gem provides simple Ruby API for calling Oracle PL/SQL procedures.
11
+ It could be used both for accessing Oracle PL/SQL API procedures in legacy applications
12
+ as well as it could be used to create PL/SQL unit tests using Ruby testing libraries.
13
+ EOS
14
+ gem.email = "raimonds.simanovskis@gmail.com"
15
+ gem.homepage = "http://github.com/rsim/ruby-plsql"
16
+ gem.authors = ["Raimonds Simanovskis"]
17
+ gem.add_development_dependency "rspec", ">= 1.2.9"
18
+ gem.extra_rdoc_files = ['README.rdoc']
19
+ end
20
+ Jeweler::GemcutterTasks.new
21
+ rescue LoadError
22
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
23
+ end
24
+
25
+ require 'spec/rake/spectask'
26
+ Spec::Rake::SpecTask.new(:spec) do |spec|
27
+ spec.libs << 'lib' << 'spec'
28
+ spec.spec_files = FileList['spec/**/*_spec.rb']
29
+ end
30
+
31
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
32
+ spec.libs << 'lib' << 'spec'
33
+ spec.pattern = 'spec/**/*_spec.rb'
34
+ spec.rcov = true
35
+ end
36
+
37
+ task :spec => :check_dependencies
38
+
39
+ task :default => :spec
40
+
41
+ require 'rake/rdoctask'
42
+ Rake::RDocTask.new do |rdoc|
43
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
44
+
45
+ rdoc.rdoc_dir = 'doc'
46
+ rdoc.title = "ruby-plsql #{version}"
47
+ rdoc.rdoc_files.include('README*')
48
+ rdoc.rdoc_files.include('lib/**/*.rb')
49
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.4.0
@@ -3,13 +3,13 @@ module PLSQL
3
3
  attr_reader :raw_driver
4
4
  attr_reader :activerecord_class
5
5
 
6
- def initialize(raw_drv, raw_conn, ar_class = nil)
6
+ def initialize(raw_drv, raw_conn, ar_class = nil) #:nodoc:
7
7
  @raw_driver = raw_drv
8
8
  @raw_connection = raw_conn
9
9
  @activerecord_class = ar_class
10
10
  end
11
11
 
12
- def self.create(raw_conn, ar_class = nil)
12
+ def self.create(raw_conn, ar_class = nil) #:nodoc:
13
13
  if ar_class && !(defined?(::ActiveRecord) && [ar_class, ar_class.superclass].include?(::ActiveRecord::Base))
14
14
  raise ArgumentError, "Wrong ActiveRecord class"
15
15
  end
@@ -24,6 +24,7 @@ module PLSQL
24
24
  end
25
25
  end
26
26
 
27
+ # Returns OCI8 or JDBC connection
27
28
  def raw_connection
28
29
  if @activerecord_class
29
30
  @activerecord_class.connection.raw_connection
@@ -32,50 +33,123 @@ module PLSQL
32
33
  end
33
34
  end
34
35
 
36
+ # Is it OCI8 connection
35
37
  def oci?
36
38
  @raw_driver == :oci
37
39
  end
38
40
 
41
+ # Is it JDBC connection
39
42
  def jdbc?
40
43
  @raw_driver == :jdbc
41
44
  end
42
45
 
43
- def logoff
46
+ def logoff #:nodoc:
44
47
  raise NoMethodError, "Not implemented for this raw driver"
45
48
  end
46
49
 
47
- def commit
50
+ def commit #:nodoc:
48
51
  raise NoMethodError, "Not implemented for this raw driver"
49
52
  end
50
53
 
51
- def rollback
54
+ def rollback #:nodoc:
52
55
  raise NoMethodError, "Not implemented for this raw driver"
53
56
  end
54
57
 
58
+ # Current autocommit mode (true or false)
55
59
  def autocommit?
56
60
  raise NoMethodError, "Not implemented for this raw driver"
57
61
  end
58
62
 
63
+ # Set autocommit mode (true or false)
59
64
  def autocommit=(value)
60
65
  raise NoMethodError, "Not implemented for this raw driver"
61
66
  end
62
67
 
63
- def select_first(sql, *bindvars)
64
- raise NoMethodError, "Not implemented for this raw driver"
68
+ def select_first(sql, *bindvars) #:nodoc:
69
+ cursor = cursor_from_query(self, sql, *bindvars)
70
+ cursor.fetch
71
+ ensure
72
+ cursor.close rescue nil
65
73
  end
66
74
 
67
- def select_all(sql, *bindvars, &block)
68
- raise NoMethodError, "Not implemented for this raw driver"
75
+ def select_hash_first(sql, *bindvars) #:nodoc:
76
+ cursor = cursor_from_query(self, sql, *bindvars)
77
+ cursor.fetch_hash
78
+ ensure
79
+ cursor.close rescue nil
80
+ end
81
+
82
+ def select_all(sql, *bindvars, &block) #:nodoc:
83
+ cursor = cursor_from_query(self, sql, *bindvars)
84
+ results = []
85
+ row_count = 0
86
+ while row = cursor.fetch
87
+ if block_given?
88
+ yield(row)
89
+ row_count += 1
90
+ else
91
+ results << row
92
+ end
93
+ end
94
+ block_given? ? row_count : results
95
+ ensure
96
+ cursor.close rescue nil
69
97
  end
70
98
 
71
- def exec(sql, *bindvars)
99
+ def select_hash_all(sql, *bindvars, &block) #:nodoc:
100
+ cursor = cursor_from_query(self, sql, *bindvars)
101
+ results = []
102
+ row_count = 0
103
+ while row = cursor.fetch_hash
104
+ if block_given?
105
+ yield(row)
106
+ row_count += 1
107
+ else
108
+ results << row
109
+ end
110
+ end
111
+ block_given? ? row_count : results
112
+ ensure
113
+ cursor.close rescue nil
114
+ end
115
+
116
+ def exec(sql, *bindvars) #:nodoc:
72
117
  raise NoMethodError, "Not implemented for this raw driver"
73
118
  end
74
119
 
75
- def parse(sql)
120
+ def parse(sql) #:nodoc:
76
121
  raise NoMethodError, "Not implemented for this raw driver"
77
122
  end
78
123
 
124
+ def arrays_to_hash(keys, values) #:nodoc:
125
+ (0...keys.size).inject({}) { |hash, i| hash[keys[i]] = values[i]; hash }
126
+ end
127
+
128
+ module CursorCommon
129
+ # Fetch all rows from cursor, each row as array of values
130
+ def fetch_all
131
+ rows = []
132
+ while (row = fetch)
133
+ rows << row
134
+ end
135
+ rows
136
+ end
137
+
138
+ # Fetch all rows from cursor, each row as hash {:column => value, ...}
139
+ def fetch_hash_all
140
+ rows = []
141
+ while (row = fetch_hash)
142
+ rows << row
143
+ end
144
+ rows
145
+ end
146
+
147
+ # Fetch row from cursor as hash {:column => value, ...}
148
+ def fetch_hash
149
+ (row = fetch) && @connection.arrays_to_hash(fields, row)
150
+ end
151
+ end
152
+
79
153
  end
80
154
 
81
155
  end
@@ -1,5 +1,36 @@
1
+ begin
2
+ require "java"
3
+ require "jruby"
4
+
5
+ # ojdbc14.jar file should be in JRUBY_HOME/lib or should be in ENV['PATH'] or load path
6
+
7
+ ojdbc_jar = "ojdbc14.jar"
8
+
9
+ unless ENV_JAVA['java.class.path'] =~ Regexp.new(ojdbc_jar)
10
+ # On Unix environment variable should be PATH, on Windows it is sometimes Path
11
+ env_path = ENV["PATH"] || ENV["Path"] || ''
12
+ if ojdbc_jar_path = env_path.split(/[:;]/).concat($LOAD_PATH).find{|d| File.exists?(File.join(d,ojdbc_jar))}
13
+ require File.join(ojdbc_jar_path,ojdbc_jar)
14
+ end
15
+ end
16
+
17
+ java.sql.DriverManager.registerDriver Java::oracle.jdbc.driver.OracleDriver.new
18
+
19
+ # set tns_admin property from TNS_ADMIN environment variable
20
+ if !java.lang.System.get_property("oracle.net.tns_admin") && ENV["TNS_ADMIN"]
21
+ java.lang.System.set_property("oracle.net.tns_admin", ENV["TNS_ADMIN"])
22
+ end
23
+
24
+ rescue LoadError, NameError
25
+ # JDBC driver is unavailable.
26
+ error_message = "ERROR: ruby-plsql could not load Oracle JDBC driver. "+
27
+ "Please install ojdbc14.jar library."
28
+ STDERR.puts error_message
29
+ raise LoadError
30
+ end
31
+
1
32
  module PLSQL
2
- class JDBCConnection < Connection
33
+ class JDBCConnection < Connection #:nodoc:
3
34
  def logoff
4
35
  raw_connection.close
5
36
  true
@@ -23,47 +54,6 @@ module PLSQL
23
54
  raw_connection.setAutoCommit(value)
24
55
  end
25
56
 
26
- def select_first(sql, *bindvars)
27
- stmt = prepare_statement(sql, *bindvars)
28
- rset = stmt.executeQuery
29
- metadata = rset.getMetaData
30
- column_count = metadata.getColumnCount
31
- if rset.next
32
- (1..column_count).map do |i|
33
- get_ruby_value_from_result_set(rset,i,metadata.getColumnTypeName(i))
34
- end
35
- else
36
- nil
37
- end
38
- ensure
39
- rset.close rescue nil
40
- stmt.close rescue nil
41
- end
42
-
43
- def select_all(sql, *bindvars, &block)
44
- stmt = prepare_statement(sql, *bindvars)
45
- results = []
46
- row_count = 0
47
- rset = stmt.executeQuery
48
- metadata = rset.getMetaData
49
- column_count = metadata.getColumnCount
50
- while rset.next
51
- row_with_typecast = (1..column_count).map do |i|
52
- get_ruby_value_from_result_set(rset,i,metadata.getColumnTypeName(i))
53
- end
54
- if block_given?
55
- yield(row_with_typecast)
56
- row_count += 1
57
- else
58
- results << row_with_typecast
59
- end
60
- end
61
- block_given? ? row_count : results
62
- ensure
63
- rset.close rescue nil
64
- stmt.close rescue nil
65
- end
66
-
67
57
  def exec(sql, *bindvars)
68
58
  cs = prepare_call(sql, *bindvars)
69
59
  cs.execute
@@ -72,9 +62,9 @@ module PLSQL
72
62
  cs.close rescue nil
73
63
  end
74
64
 
75
- class Cursor
65
+ class CallableStatement #:nodoc:
76
66
 
77
- def initialize(sql, conn)
67
+ def initialize(conn, sql)
78
68
  @sql = sql
79
69
  @connection = conn
80
70
  @params = sql.scan(/\:\w+/)
@@ -83,12 +73,19 @@ module PLSQL
83
73
  @statement = @connection.prepare_call(sql)
84
74
  end
85
75
 
86
- def bind_param(key, value, type=nil, length=nil, in_out='IN')
87
- @connection.set_bind_variable(@statement, key, value, type, length)
88
- if in_out =~ /OUT/
89
- @out_types[key] = type || value.class
90
- @out_index[key] = bind_param_index(key)
91
- @statement.registerOutParameter(@out_index[key],@connection.get_java_sql_type(value,type))
76
+ def bind_param(arg, value, metadata)
77
+ type, length = @connection.plsql_to_ruby_data_type(metadata)
78
+ ora_value = @connection.ruby_value_to_ora_value(value, type, metadata)
79
+ @connection.set_bind_variable(@statement, arg, ora_value, type, length, metadata)
80
+ if metadata[:in_out] =~ /OUT/
81
+ @out_types[arg] = type || ora_value.class
82
+ @out_index[arg] = bind_param_index(arg)
83
+ if ['TABLE','VARRAY','OBJECT','REF CURSOR'].include?(metadata[:data_type])
84
+ @statement.registerOutParameter(@out_index[arg], @connection.get_java_sql_type(ora_value,type),
85
+ metadata[:sql_type_name])
86
+ else
87
+ @statement.registerOutParameter(@out_index[arg],@connection.get_java_sql_type(ora_value,type))
88
+ end
92
89
  end
93
90
  end
94
91
 
@@ -97,7 +94,7 @@ module PLSQL
97
94
  end
98
95
 
99
96
  def [](key)
100
- @connection.get_bind_variable(@statement, @out_index[key], @out_types[key])
97
+ @connection.ora_value_to_ruby_value(@connection.get_bind_variable(@statement, @out_index[key], @out_types[key]))
101
98
  end
102
99
 
103
100
  def close
@@ -113,14 +110,68 @@ module PLSQL
113
110
  end
114
111
  end
115
112
 
113
+ class Cursor #:nodoc:
114
+ include Connection::CursorCommon
115
+
116
+ attr_reader :result_set
117
+ attr_accessor :statement
118
+
119
+ def initialize(conn, result_set)
120
+ @connection = conn
121
+ @result_set = result_set
122
+ @metadata = @result_set.getMetaData
123
+ @column_count = @metadata.getColumnCount
124
+ @column_type_names = [nil] # column numbering starts at 1
125
+ (1..@column_count).each do |i|
126
+ @column_type_names << {:type_name => @metadata.getColumnTypeName(i), :sql_type => @metadata.getColumnType(i)}
127
+ end
128
+ end
129
+
130
+ def self.new_from_query(conn, sql, *bindvars)
131
+ stmt = conn.prepare_statement(sql, *bindvars)
132
+ cursor = Cursor.new(conn, stmt.executeQuery)
133
+ cursor.statement = stmt
134
+ cursor
135
+ rescue
136
+ # in case of any error close statement
137
+ stmt.close rescue nil
138
+ raise
139
+ end
140
+
141
+ def fetch
142
+ if @result_set.next
143
+ (1..@column_count).map do |i|
144
+ @connection.get_ruby_value_from_result_set(@result_set, i, @column_type_names[i])
145
+ end
146
+ else
147
+ nil
148
+ end
149
+ end
150
+
151
+ def fields
152
+ @fields ||= (1..@column_count).map do |i|
153
+ @metadata.getColumnName(i).downcase.to_sym
154
+ end
155
+ end
156
+
157
+ def close
158
+ @result_set.close
159
+ @statement.close if @statement
160
+ end
161
+ end
162
+
116
163
  def parse(sql)
117
- Cursor.new(sql, self)
164
+ CallableStatement.new(self, sql)
165
+ end
166
+
167
+ def cursor_from_query(sql, *bindvars)
168
+ Cursor.new_from_query(sql, *bindvars)
118
169
  end
119
170
 
120
171
  def prepare_statement(sql, *bindvars)
121
172
  stmt = raw_connection.prepareStatement(sql)
122
173
  bindvars.each_with_index do |bv, i|
123
- set_bind_variable(stmt, i+1, bv)
174
+ set_bind_variable(stmt, i+1, ruby_value_to_ora_value(bv))
124
175
  end
125
176
  stmt
126
177
  end
@@ -133,118 +184,129 @@ module PLSQL
133
184
  stmt
134
185
  end
135
186
 
187
+ RUBY_CLASS_TO_SQL_TYPE = {
188
+ Fixnum => java.sql.Types::INTEGER,
189
+ Bignum => java.sql.Types::INTEGER,
190
+ Integer => java.sql.Types::INTEGER,
191
+ Float => java.sql.Types::FLOAT,
192
+ BigDecimal => java.sql.Types::NUMERIC,
193
+ String => java.sql.Types::VARCHAR,
194
+ Java::OracleSql::CLOB => Java::oracle.jdbc.OracleTypes::CLOB,
195
+ Java::OracleSql::BLOB => Java::oracle.jdbc.OracleTypes::BLOB,
196
+ Date => java.sql.Types::DATE,
197
+ Time => java.sql.Types::DATE,
198
+ DateTime => java.sql.Types::DATE,
199
+ Java::OracleSql::ARRAY => Java::oracle.jdbc.OracleTypes::ARRAY,
200
+ Array => Java::oracle.jdbc.OracleTypes::ARRAY,
201
+ Java::OracleSql::STRUCT => Java::oracle.jdbc.OracleTypes::STRUCT,
202
+ Hash => Java::oracle.jdbc.OracleTypes::STRUCT,
203
+ java.sql.ResultSet => Java::oracle.jdbc.OracleTypes::CURSOR,
204
+ }
205
+
206
+ SQL_TYPE_TO_RUBY_CLASS = {
207
+ java.sql.Types::CHAR => String,
208
+ java.sql.Types::VARCHAR => String,
209
+ java.sql.Types::NUMERIC => BigDecimal,
210
+ java.sql.Types::DATE => Time,
211
+ java.sql.Types::TIMESTAMP => Time,
212
+ Java::oracle.jdbc.OracleTypes::TIMESTAMPTZ => Time,
213
+ Java::oracle.jdbc.OracleTypes::TIMESTAMPLTZ => Time,
214
+ java.sql.Types::BLOB => String,
215
+ java.sql.Types::CLOB => String,
216
+ java.sql.Types::ARRAY => Java::OracleSql::ARRAY,
217
+ java.sql.Types::STRUCT => Java::OracleSql::STRUCT,
218
+ Java::oracle.jdbc.OracleTypes::CURSOR => java.sql.ResultSet
219
+ }
220
+
136
221
  def get_java_sql_type(value, type)
137
- case type ? type.to_s : value.class.to_s
138
- when 'Fixnum', 'Bignum', 'Integer'
139
- java.sql.Types::INTEGER
140
- when 'Float'
141
- java.sql.Types::FLOAT
142
- when 'BigDecimal'
143
- java.sql.Types::NUMERIC
144
- when 'String'
145
- java.sql.Types::VARCHAR
146
- when 'Java::OracleSql::CLOB'
147
- Java::oracle.jdbc.OracleTypes::CLOB
148
- when 'Java::OracleSql::BLOB'
149
- Java::oracle.jdbc.OracleTypes::BLOB
150
- when 'Date'
151
- java.sql.Types::DATE
152
- when 'Time'
153
- java.sql.Types::DATE
154
- when 'DateTime'
155
- java.sql.Types::DATE
156
- else
157
- java.sql.Types::VARCHAR
158
- end
222
+ RUBY_CLASS_TO_SQL_TYPE[type || value.class] || java.sql.Types::VARCHAR
159
223
  end
160
224
 
161
- def set_bind_variable(stmt, i, value, type=nil, length=nil)
225
+ def set_bind_variable(stmt, i, value, type=nil, length=nil, metadata={})
162
226
  key = i.kind_of?(Integer) ? nil : i.to_s.gsub(':','')
163
- case !value.nil? && type ? type.to_s : value.class.to_s
164
- when 'Fixnum', 'Bignum', 'Integer'
227
+ type_symbol = (!value.nil? && type ? type : value.class).to_s.to_sym
228
+ case type_symbol
229
+ when :Fixnum, :Bignum, :Integer
165
230
  stmt.send("setInt#{key && "AtName"}", key || i, value)
166
- when 'Float'
231
+ when :Float
167
232
  stmt.send("setFloat#{key && "AtName"}", key || i, value)
168
- when 'BigDecimal'
169
- stmt.send("setBigDecimal#{key && "AtName"}", key || i, java.math.BigDecimal.new(value.to_s))
170
- when 'String'
233
+ when :BigDecimal, :'Java::JavaMath::BigDecimal'
234
+ stmt.send("setBigDecimal#{key && "AtName"}", key || i, value)
235
+ when :String
171
236
  stmt.send("setString#{key && "AtName"}", key || i, value)
172
- when 'Java::OracleSql::CLOB'
237
+ when :'Java::OracleSql::CLOB'
173
238
  stmt.send("setClob#{key && "AtName"}", key || i, value)
174
- when 'Java::OracleSql::BLOB'
239
+ when :'Java::OracleSql::BLOB'
175
240
  stmt.send("setBlob#{key && "AtName"}", key || i, value)
176
- when 'Date', 'Time', 'DateTime'
177
- stmt.send("setDATE#{key && "AtName"}", key || i, Java::oracle.sql.DATE.new(value.strftime("%Y-%m-%d %H:%M:%S")))
178
- when 'NilClass'
179
- stmt.send("setNull#{key && "AtName"}", key || i, get_java_sql_type(value, type))
241
+ when :Date, :Time, :DateTime, :'Java::OracleSql::DATE'
242
+ stmt.send("setDATE#{key && "AtName"}", key || i, value)
243
+ when :NilClass
244
+ if ['TABLE', 'VARRAY', 'OBJECT'].include?(metadata[:data_type])
245
+ stmt.send("setNull#{key && "AtName"}", key || i, get_java_sql_type(value, type),
246
+ metadata[:sql_type_name])
247
+ elsif metadata[:data_type] == 'REF CURSOR'
248
+ # TODO: cannot bind NULL value to cursor parameter, getting error
249
+ # java.sql.SQLException: Unsupported feature: sqlType=-10
250
+ # Currently do nothing and assume that NULL values will not be passed to IN parameters
251
+ # If cursor is IN/OUT or OUT parameter then it should work
252
+ else
253
+ stmt.send("setNull#{key && "AtName"}", key || i, get_java_sql_type(value, type))
254
+ end
255
+ when :'Java::OracleSql::ARRAY'
256
+ stmt.send("setARRAY#{key && "AtName"}", key || i, value)
257
+ when :'Java::OracleSql::STRUCT'
258
+ stmt.send("setSTRUCT#{key && "AtName"}", key || i, value)
259
+ when :'Java::JavaSql::ResultSet'
260
+ # TODO: cannot find how to pass cursor parameter from JDBC
261
+ # setCursor is giving exception java.sql.SQLException: Unsupported feature
262
+ stmt.send("setCursor#{key && "AtName"}", key || i, value)
263
+ else
264
+ raise ArgumentError, "Don't know how to bind variable with type #{type_symbol}"
180
265
  end
181
266
  end
182
267
 
183
268
  def get_bind_variable(stmt, i, type)
184
- case type.to_s
185
- when 'Fixnum', 'Bignum', 'Integer'
269
+ case type.to_s.to_sym
270
+ when :Fixnum, :Bignum, :Integer
186
271
  stmt.getInt(i)
187
- when 'Float'
272
+ when :Float
188
273
  stmt.getFloat(i)
189
- when 'BigDecimal'
274
+ when :BigDecimal
190
275
  bd = stmt.getBigDecimal(i)
191
276
  bd && BigDecimal.new(bd.to_s)
192
- when 'String'
277
+ when :String
193
278
  stmt.getString(i)
194
- when 'Java::OracleSql::CLOB'
279
+ when :'Java::OracleSql::CLOB'
195
280
  stmt.getClob(i)
196
- when 'Java::OracleSql::BLOB'
281
+ when :'Java::OracleSql::BLOB'
197
282
  stmt.getBlob(i)
198
- when 'Date','Time','DateTime'
199
- if dt = stmt.getDATE(i)
200
- d = dt.dateValue
201
- t = dt.timeValue
202
- Time.send(plsql.default_timezone, d.year + 1900, d.month + 1, d.date, t.hours, t.minutes, t.seconds)
203
- else
204
- nil
205
- end
283
+ when :Date, :Time, :DateTime
284
+ stmt.getDATE(i)
285
+ when :'Java::OracleSql::ARRAY'
286
+ stmt.getArray(i)
287
+ when :'Java::OracleSql::STRUCT'
288
+ stmt.getSTRUCT(i)
289
+ when :'Java::JavaSql::ResultSet'
290
+ stmt.getCursor(i)
206
291
  end
207
292
  end
208
293
 
209
- def get_ruby_value_from_result_set(rset, i, type_name)
210
- case type_name
211
- when "CHAR", "VARCHAR2"
212
- rset.getString(i)
213
- when "CLOB"
214
- ora_value_to_ruby_value(rset.getClob(i))
215
- when "BLOB"
216
- ora_value_to_ruby_value(rset.getBlob(i))
217
- when "NUMBER"
218
- d = rset.getBigDecimal(i)
219
- if d.nil?
220
- nil
221
- elsif d.scale == 0
222
- d.toBigInteger+0
223
- else
224
- BigDecimal(d.toString)
225
- end
226
- when "DATE"
227
- if dt = rset.getDATE(i)
228
- d = dt.dateValue
229
- t = dt.timeValue
230
- Time.send(plsql.default_timezone, d.year + 1900, d.month + 1, d.date, t.hours, t.minutes, t.seconds)
231
- else
232
- nil
233
- end
234
- when /^TIMESTAMP/
235
- ts = rset.getTimestamp(i)
236
- ts && Time.send(Base.default_timezone, ts.year + 1900, ts.month + 1, ts.date, ts.hours, ts.minutes, ts.seconds,
237
- ts.nanos / 1000)
238
- else
239
- nil
240
- end
294
+ def get_ruby_value_from_result_set(rset, i, metadata)
295
+ ruby_type = SQL_TYPE_TO_RUBY_CLASS[metadata[:sql_type]]
296
+ ora_value = get_bind_variable(rset, i, ruby_type)
297
+ result_new = ora_value_to_ruby_value(ora_value)
241
298
  end
242
-
243
- def plsql_to_ruby_data_type(data_type, data_length)
299
+
300
+ def result_set_to_ruby_data_type(column_type, column_type_name)
301
+
302
+ end
303
+
304
+ def plsql_to_ruby_data_type(metadata)
305
+ data_type, data_length = metadata[:data_type], metadata[:data_length]
244
306
  case data_type
245
- when "VARCHAR2"
307
+ when "VARCHAR2", "CHAR", "NVARCHAR2", "NCHAR"
246
308
  [String, data_length || 32767]
247
- when "CLOB"
309
+ when "CLOB", "NCLOB"
248
310
  [Java::OracleSql::CLOB, nil]
249
311
  when "BLOB"
250
312
  [Java::OracleSql::BLOB, nil]
@@ -252,73 +314,163 @@ module PLSQL
252
314
  [BigDecimal, nil]
253
315
  when "DATE"
254
316
  [Time, nil]
255
- when "TIMESTAMP"
317
+ when "TIMESTAMP", "TIMESTAMP WITH TIME ZONE", "TIMESTAMP WITH LOCAL TIME ZONE"
256
318
  [Time, nil]
319
+ when "TABLE", "VARRAY"
320
+ [Java::OracleSql::ARRAY, nil]
321
+ when "OBJECT"
322
+ [Java::OracleSql::STRUCT, nil]
323
+ when "REF CURSOR"
324
+ [java.sql.ResultSet, nil]
257
325
  else
258
326
  [String, 32767]
259
327
  end
260
328
  end
261
329
 
262
- def ruby_value_to_ora_value(val, type)
263
- if type == BigDecimal
264
- val.nil? || val.is_a?(Fixnum) || val.is_a?(BigDecimal) ? val : BigDecimal(val.to_s)
265
- elsif type == Time
266
- case val
330
+ def ruby_value_to_ora_value(value, type=nil, metadata={})
331
+ type ||= value.class
332
+ case type.to_s.to_sym
333
+ when :Fixnum, :String
334
+ value
335
+ when :BigDecimal
336
+ case value
337
+ when TrueClass
338
+ java_bigdecimal(1)
339
+ when FalseClass
340
+ java_bigdecimal(0)
341
+ else
342
+ java_bigdecimal(value)
343
+ end
344
+ when :Time, :Date, :DateTime
345
+ case value
267
346
  when DateTime
268
- Time.send(plsql.default_timezone, val.year, val.month, val.day, val.hour, val.min, val.sec)
347
+ java_date(Time.send(plsql.default_timezone, value.year, value.month, value.day, value.hour, value.min, value.sec))
269
348
  when Date
270
- Time.send(plsql.default_timezone, val.year, val.month, val.day, 0, 0, 0)
349
+ java_date(Time.send(plsql.default_timezone, value.year, value.month, value.day, 0, 0, 0))
271
350
  else
272
- val
351
+ java_date(value)
273
352
  end
274
- elsif type == Java::OracleSql::CLOB
275
- if val
353
+ when :'Java::OracleSql::CLOB'
354
+ if value
276
355
  clob = Java::OracleSql::CLOB.createTemporary(raw_connection, false, Java::OracleSql::CLOB::DURATION_SESSION)
277
- clob.setString(1,val)
356
+ clob.setString(1, value)
278
357
  clob
279
358
  else
280
359
  Java::OracleSql::CLOB.getEmptyCLOB
281
360
  end
282
- elsif type == Java::OracleSql::BLOB
283
- if val
361
+ when :'Java::OracleSql::BLOB'
362
+ if value
284
363
  blob = Java::OracleSql::BLOB.createTemporary(raw_connection, false, Java::OracleSql::BLOB::DURATION_SESSION)
285
- blob.setBytes(1, val.to_java_bytes)
364
+ blob.setBytes(1, value.to_java_bytes)
286
365
  blob
287
366
  else
288
367
  Java::OracleSql::BLOB.getEmptyBLOB
289
368
  end
369
+ when :'Java::OracleSql::ARRAY'
370
+ if value
371
+ raise ArgumentError, "You should pass Array value for collection type parameter" unless value.is_a?(Array)
372
+ descriptor = Java::OracleSql::ArrayDescriptor.createDescriptor(metadata[:sql_type_name], raw_connection)
373
+ elem_type = descriptor.getBaseType
374
+ elem_type_name = descriptor.getBaseName
375
+ elem_list = value.map do |elem|
376
+ case elem_type
377
+ when Java::oracle.jdbc.OracleTypes::ARRAY
378
+ ruby_value_to_ora_value(elem, Java::OracleSql::ARRAY, :sql_type_name => elem_type_name)
379
+ when Java::oracle.jdbc.OracleTypes::STRUCT
380
+ ruby_value_to_ora_value(elem, Java::OracleSql::STRUCT, :sql_type_name => elem_type_name)
381
+ else
382
+ ruby_value_to_ora_value(elem)
383
+ end
384
+ end
385
+ Java::OracleSql::ARRAY.new(descriptor, raw_connection, elem_list.to_java)
386
+ end
387
+ when :'Java::OracleSql::STRUCT'
388
+ if value
389
+ raise ArgumentError, "You should pass Hash value for object type parameter" unless value.is_a?(Hash)
390
+ descriptor = Java::OracleSql::StructDescriptor.createDescriptor(metadata[:sql_type_name], raw_connection)
391
+ struct_metadata = descriptor.getMetaData
392
+ struct_fields = (1..descriptor.getLength).inject({}) do |hash, i|
393
+ hash[struct_metadata.getColumnName(i).downcase.to_sym] =
394
+ {:type => struct_metadata.getColumnType(i), :type_name => struct_metadata.getColumnTypeName(i)}
395
+ hash
396
+ end
397
+ object_attrs = java.util.HashMap.new
398
+ value.each do |key, attr_value|
399
+ raise ArgumentError, "Wrong object type field passed to PL/SQL procedure" unless (field = struct_fields[key])
400
+ case field[:type]
401
+ when Java::oracle.jdbc.OracleTypes::ARRAY
402
+ # nested collection
403
+ object_attrs.put(key.to_s.upcase, ruby_value_to_ora_value(attr_value, Java::OracleSql::ARRAY, :sql_type_name => field[:type_name]))
404
+ when Java::oracle.jdbc.OracleTypes::STRUCT
405
+ # nested object type
406
+ object_attrs.put(key.to_s.upcase, ruby_value_to_ora_value(attr_value, Java::OracleSql::STRUCT, :sql_type_name => field[:type_name]))
407
+ else
408
+ object_attrs.put(key.to_s.upcase, ruby_value_to_ora_value(attr_value))
409
+ end
410
+ end
411
+ Java::OracleSql::STRUCT.new(descriptor, raw_connection, object_attrs)
412
+ end
413
+ when :'Java::JavaSql::ResultSet'
414
+ if value
415
+ value.result_set
416
+ end
290
417
  else
291
- val
418
+ value
292
419
  end
293
420
  end
294
421
 
295
- def ora_value_to_ruby_value(val)
296
- case val
422
+ def ora_value_to_ruby_value(value)
423
+ case value
297
424
  when Float, BigDecimal
298
- ora_number_to_ruby_number(val)
425
+ ora_number_to_ruby_number(value)
426
+ when Java::JavaMath::BigDecimal
427
+ value && ora_number_to_ruby_number(BigDecimal.new(value.to_s))
428
+ when Java::OracleSql::DATE
429
+ if value
430
+ d = value.dateValue
431
+ t = value.timeValue
432
+ Time.send(plsql.default_timezone, d.year + 1900, d.month + 1, d.date, t.hours, t.minutes, t.seconds)
433
+ end
299
434
  when Java::OracleSql::CLOB
300
- if val.isEmptyLob
435
+ if value.isEmptyLob
301
436
  nil
302
437
  else
303
- val.getSubString(1, val.length)
438
+ value.getSubString(1, value.length)
304
439
  end
305
440
  when Java::OracleSql::BLOB
306
- if val.isEmptyLob
441
+ if value.isEmptyLob
307
442
  nil
308
443
  else
309
- String.from_java_bytes(val.getBytes(1, val.length))
444
+ String.from_java_bytes(value.getBytes(1, value.length))
310
445
  end
446
+ when Java::OracleSql::ARRAY
447
+ value.getArray.map{|e| ora_value_to_ruby_value(e)}
448
+ when Java::OracleSql::STRUCT
449
+ descriptor = value.getDescriptor
450
+ struct_metadata = descriptor.getMetaData
451
+ field_names = (1..descriptor.getLength).map {|i| struct_metadata.getColumnName(i).downcase.to_sym}
452
+ field_values = value.getAttributes.map{|e| ora_value_to_ruby_value(e)}
453
+ arrays_to_hash(field_names, field_values)
454
+ when Java::java.sql.ResultSet
455
+ Cursor.new(self, value)
311
456
  else
312
- val
457
+ value
313
458
  end
314
459
  end
315
460
 
316
461
  private
317
462
 
463
+ def java_date(value)
464
+ value && Java::oracle.sql.DATE.new(value.strftime("%Y-%m-%d %H:%M:%S"))
465
+ end
466
+
467
+ def java_bigdecimal(value)
468
+ value && java.math.BigDecimal.new(value.to_s)
469
+ end
470
+
318
471
  def ora_number_to_ruby_number(num)
319
472
  # return BigDecimal instead of Float to avoid rounding errors
320
- # num.to_i == num.to_f ? num.to_i : num.to_f
321
- num == (num_to_i = num.to_i) ? num_to_i : BigDecimal.new(num.to_s)
473
+ num == (num_to_i = num.to_i) ? num_to_i : (num.is_a?(BigDecimal) ? num : BigDecimal.new(num.to_s))
322
474
  end
323
475
 
324
476
  end