ruby-plsql 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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