jdbc-helper 0.4.2 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -85,6 +85,7 @@ Add JDBC driver of the DBMS you're willing to use to your CLASSPATH
85
85
  else
86
86
  tx.rollback
87
87
  end
88
+ # You never reach here.
88
89
  end
89
90
 
90
91
  === Using batch interface
@@ -155,8 +156,8 @@ Add JDBC driver of the DBMS you're willing to use to your CLASSPATH
155
156
  table.where(:c => 3).delete
156
157
 
157
158
  # Truncate or drop table (Cannot be undone)
158
- table.truncate_table!
159
- table.drop_table!
159
+ table.truncate!
160
+ table.drop!
160
161
 
161
162
  === Using function wrappers (since 0.2.2)
162
163
  conn.function(:mod).call 5, 3
@@ -166,58 +167,15 @@ Add JDBC driver of the DBMS you're willing to use to your CLASSPATH
166
167
  # Working with IN/INOUT/OUT parameteres
167
168
  # Bind by ordinal number
168
169
  conn.procedure(:update_and_fetch_something).call(
169
- 100, ["value", String], Fixnum)
170
+ 100, # Input parameter
171
+ ["value", String], # Input/Output parameter
172
+ Fixnum # Output parameter
173
+ )
170
174
 
171
175
  # Bind by parameter name
172
176
  conn.procedure(:update_and_fetch_something).call(
173
177
  :a => 100, :b => ["value", String], :c => Fixnum)
174
178
 
175
- == Notes on vendor-independence
176
-
177
- jdbc-helper tries to be a vendor-independent library, so that it behaves the same on any RDBMS,
178
- and that is why it's built on JDBC in the first place which can be used to minimize the amount of vendor-specific code.
179
- (Ideally, one codebase for all RDBMSs.)
180
- So far so good, but not great. It has small amount of code that is vendor-specific, non-standard JDBC.
181
- And it has been confirmed to work the same for both MySQL and Oracle, except for stored procedures.
182
- MySQL and Oracle implement parameter binding of CallableStatement differently.
183
- See the following example.
184
- # Let's say we have a stored procedure named my_procedure
185
- # which takes 3 parameters, param1, param2, and param3.
186
-
187
- # Creates a CallableStatemet
188
- cstmt = conn.prepare_call("{call my_procedure(?, ?, ?)}")
189
-
190
- cstmt.call(10, "ten", Fixnum)
191
- # OK for both MySQL and Oracle. Filling paramters sequentially.
192
-
193
- cstmt.call(:param1 => 10, :param2 => "ten", :param3 => Fixnum)
194
- # MySQL automatically binds values by the parameter names in the original procedure definition.
195
- # Oracle fails to do so.
196
- cstmt.close
197
-
198
- # For Oracle, if you prepare as follows,
199
- cstmt = conn.prepare_call("{call my_procedure(:p1, :p2, :p3)}")
200
-
201
- # Then you can do this.
202
- cstmt.call(:p1 => 10, :p2 => "ten", :p3 => Fixnum)
203
- # However, p1, p2, p3 have nothing to do with the original
204
- # parameter names (param1, param2, param3) in the procedure definition.
205
- # They're just aliases for ordinal positions.
206
- # So, it's semantically different from the first example.
207
-
208
- cstmt.close
209
-
210
- There's no way to find out the original parameter names and their ordinal positions without looking up the metadata of the database,
211
- which requires having to write vendor-specific code branches, and that is definitely not desirable.
212
- ProcedureWrapper prepares call with '?'s as the first example.
213
- So it would work fine for MySQL, but you can't use it with parameter names on Oracle.
214
-
215
- # Good for both
216
- conn.procedure(:my_procedure).call(10, "ten", Fixnum)
217
-
218
- # Only for MySQL
219
- conn.procedure(:my_procedure).call(:param1 => 10, :param2 => "ten", :param3 => Fixnum)
220
-
221
179
  == Contributing to jdbc-helper
222
180
 
223
181
  * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
@@ -41,8 +41,7 @@ private
41
41
 
42
42
  out_params = {}
43
43
  hash_params.each do | idx, value |
44
- # Symbol need to be transformed into string
45
-
44
+ # Symbols need to be transformed into string
46
45
  idx_ = idx.is_a?(Symbol) ? idx.to_s : idx
47
46
  case value
48
47
  # OUT parameter
@@ -5,7 +5,7 @@ module JDBCHelper
5
5
  # A wrapper object representing a database procedure.
6
6
  # @since 0.3.0
7
7
  # @example Usage
8
- # conn.function(:coalesce).call(nil, nil, 'king')
8
+ # conn.procedure(:my_procedure).call(1, ["a", String], Fixnum)
9
9
  class ProcedureWrapper < ObjectWrapper
10
10
  # Returns the name of the procedure
11
11
  # @return [String]
@@ -14,15 +14,124 @@ class ProcedureWrapper < ObjectWrapper
14
14
  # Executes the procedure and returns the values of INOUT & OUT parameters in Hash
15
15
  # @return [Hash]
16
16
  def call(*args)
17
- param_count = args.first.kind_of?(Hash) ? args.first.keys.length : args.length
18
-
19
- cstmt = @connection.prepare_call "{call #{name}(#{Array.new(param_count){'?'}.join ', '})}"
17
+ params = build_params args
18
+ cstmt = @connection.prepare_call "{call #{name}(#{Array.new(@cols.length){'?'}.join ', '})}"
20
19
  begin
21
- cstmt.call *args
20
+ process_result( args, cstmt.call(*params) )
22
21
  ensure
23
22
  cstmt.close
24
23
  end
25
24
  end
25
+
26
+ # Reloads procedure metadata. Metadata is cached for performance.
27
+ # However, if you have modified the procedure, you need to reload the
28
+ # metadata with this method.
29
+ # @return [JDBCHelper::ProcedureWrapper]
30
+ def refresh
31
+ @cols = @defaults = nil
32
+ self
33
+ end
34
+
35
+ private
36
+ def metadata_lookup
37
+ return if @cols
38
+
39
+ procedure, package, schema = name.split('.').reverse
40
+ procedure_u, package_u, schema_u = name.upcase.split('.').reverse
41
+
42
+ # Alas, metadata lookup can be case-sensitive. e.g. Oracle
43
+ dbmd = @connection.java_obj.get_meta_data
44
+ lookups =
45
+ if schema
46
+ [
47
+ lambda { dbmd.getProcedureColumns(package, schema, procedure, nil) },
48
+ lambda { dbmd.getProcedureColumns(package_u, schema_u, procedure_u, nil) }
49
+ ]
50
+ else
51
+ # schema or catalog? we don't know.
52
+ # - too inefficient for parameter-less procedures
53
+ [ lambda { dbmd.getProcedureColumns(nil, package, procedure, nil) },
54
+ lambda { dbmd.getProcedureColumns(nil, package_u, procedure_u, nil) },
55
+ lambda { dbmd.getProcedureColumns(package, nil, procedure, nil) },
56
+ lambda { dbmd.getProcedureColumns(package_u, nil, procedure_u, nil) },
57
+ ]
58
+ end
59
+
60
+ cols = []
61
+ @defaults = {}
62
+ lookups.each do |ld|
63
+ rset = ld.call
64
+ default_support = rset.get_meta_data.get_column_count >= 14
65
+ while rset.next
66
+ next if rset.getString("COLUMN_TYPE") == Java::JavaSql::DatabaseMetaData.procedureColumnReturn
67
+
68
+ cols << rset.getString("COLUMN_NAME").upcase
69
+ # TODO/NOT TESTED
70
+ # - MySQL doesn't support default values for procedure parameters
71
+ # - Oracle supports default values, however it does not provide
72
+ # standard interface to query default values. COLUMN_DEF always returns null.
73
+ # http://download.oracle.com/docs/cd/E11882_01/appdev.112/e13995/oracle/jdbc/OracleDatabaseMetaData.html#getProcedureColumns_java_lang_String__java_lang_String__java_lang_String__java_lang_String_
74
+ if default_support && (default = rset.getString("COLUMN_DEF"))
75
+ @defaults[cols.length - 1] << default
76
+ end
77
+ end
78
+ unless cols.empty?
79
+ @cols = cols
80
+ break
81
+ end
82
+ end
83
+ @cols ||= []
84
+ end
85
+
86
+ def build_params args
87
+ metadata_lookup
88
+
89
+ params = []
90
+ # Array
91
+ unless args.first.kind_of?(Hash)
92
+ if args.length < @cols.length
93
+ # Fill the Array with default values
94
+ @cols.length.times do |idx|
95
+ if @defaults[idx]
96
+ params << @defaults[idx]
97
+ else
98
+ params << args[idx]
99
+ end
100
+ end
101
+ else
102
+ params = args
103
+ end
104
+
105
+ # Hash
106
+ else
107
+ raise ArgumentError.new("More than one Hash given") if args.length > 1
108
+
109
+ # Set to default,
110
+ @defaults.each do |idx, v|
111
+ params[idx] = v
112
+ end
113
+
114
+ # then override
115
+ args.first.each do |k, v|
116
+ idx = @cols.index k.to_s.upcase
117
+ params[idx] = v
118
+ end
119
+ end
120
+
121
+ return params
122
+ end
123
+
124
+ def process_result args, result
125
+ input = args.first
126
+ return result unless input.kind_of? Hash
127
+
128
+ final = {}
129
+ result.each do |idx, ret|
130
+ key = input.keys.find { |key| key.to_s.upcase == @cols[idx - 1] }
131
+ final[key] = ret
132
+ end
133
+ final
134
+ end
26
135
  end#ProcedureWrapper
27
136
  end#JDBCHelper
28
137
 
data/test/database.yml CHANGED
@@ -2,12 +2,21 @@
2
2
  mysql:
3
3
  driver: com.mysql.jdbc.Driver
4
4
  url: jdbc:mysql://localhost/test
5
+ database: test
5
6
  user: root
6
7
  password:
7
8
 
9
+ #oracle:
10
+ #driver: oracle.jdbc.driver.OracleDriver
11
+ #url: jdbc:oracle:thin:@localhost/svc
12
+ #database: test
13
+ #user: testuser
14
+ #password: testpassword
15
+
8
16
  oracle:
9
17
  driver: oracle.jdbc.driver.OracleDriver
10
18
  url: jdbc:oracle:thin:@10.20.253.105/didev
19
+ database: wisefn
11
20
  user: wisefn
12
21
  password: wisefndb
13
-
22
+ timeout: 5
@@ -0,0 +1,17 @@
1
+ ---
2
+ mysql:
3
+ driver: com.mysql.jdbc.Driver
4
+ url: jdbc:mysql://localhost/test
5
+ database: test
6
+ user: root
7
+ password:
8
+ timeout: 5
9
+
10
+ oracle:
11
+ driver: oracle.jdbc.driver.OracleDriver
12
+ url: jdbc:oracle:thin:@10.20.253.105/didev
13
+ database: wisefn
14
+ user: wisefn
15
+ password: wisefndb
16
+ timeout: 5
17
+
data/test/helper.rb CHANGED
@@ -22,26 +22,48 @@ module JDBCHelperTestHelper
22
22
  @db_config ||= YAML.load File.read(File.dirname(__FILE__) + '/database.yml')
23
23
  end
24
24
 
25
+ def create_test_procedure_simple conn, name
26
+ case @type
27
+ when :mysql
28
+ conn.update "drop procedure #{name}" rescue nil
29
+ conn.update("
30
+ create procedure #{name}()
31
+ select 1 from dual where 1 != 0")
32
+ when :oracle
33
+ conn.update "drop procedure #{name}" rescue nil
34
+ conn.update "
35
+ create or replace
36
+ procedure #{name} as
37
+ begin
38
+ null;
39
+ end;"
40
+ else
41
+ raise NotImplementedError.new "Procedure test not implemented for #{@type}"
42
+ end
43
+ end
44
+
25
45
  def create_test_procedure conn, name
26
46
  case @type
27
47
  when :mysql
28
48
  conn.update "drop procedure #{name}" rescue nil
29
49
  conn.update("
30
50
  create procedure #{name}
31
- (IN i1 varchar(100), IN i2 int,
51
+ (IN i1 varchar(100), IN i2 int,
32
52
  INOUT io1 int, INOUT io2 timestamp,
53
+ IN n1 int,
33
54
  OUT o1 float, OUT o2 varchar(100))
34
- select io1 * 10, 0.1, i1 into io1, o1, o2 from dual")
55
+ select io1 * i2, 0.1, i1 into io1, o1, o2 from dual where n1 is null")
35
56
  when :oracle
36
57
  conn.update "drop procedure #{name}" rescue nil
37
58
  conn.update "
38
59
  create or replace
39
60
  procedure #{name}
40
- (i1 in varchar2, i2 in int,
61
+ (i1 in varchar2, i2 in int default '1',
41
62
  io1 in out int, io2 in out date,
63
+ n1 in int,
42
64
  o1 out float, o2 out varchar2) as
43
65
  begin
44
- select io1 * 10, 0.1, i1 into io1, o1, o2 from dual;
66
+ select io1 * i2, 0.1, i1 into io1, o1, o2 from dual where n1 is null;
45
67
  end;"
46
68
  else
47
69
  raise NotImplementedError.new "Procedure test not implemented for #{@type}"
@@ -59,7 +81,7 @@ module JDBCHelperTestHelper
59
81
 
60
82
  def each_connection(&block)
61
83
  config.each do | db, conn_info |
62
- conn = JDBCHelper::Connection.new(conn_info)
84
+ conn = JDBCHelper::Connection.new(conn_info.reject { |k,v| k == 'database'})
63
85
  # Just for quick and dirty testing
64
86
  @type = case conn_info['driver'] || conn_info[:driver]
65
87
  when /mysql/i
@@ -71,7 +71,7 @@ class TestConnection < Test::Unit::TestCase
71
71
  def test_connect_and_close
72
72
  config.each do | db, conn_info_org |
73
73
  4.times do | i |
74
- conn_info = conn_info_org.dup
74
+ conn_info = conn_info_org.reject { |k,v| k == 'database' }
75
75
 
76
76
  # With or without timeout parameter
77
77
  conn_info['timeout'] = 60 if i % 2 == 1
@@ -305,22 +305,23 @@ class TestConnection < Test::Unit::TestCase
305
305
  create_test_procedure conn, TEST_PROCEDURE
306
306
 
307
307
  # Array parameter
308
- cstmt_ord = conn.prepare_call "{call #{TEST_PROCEDURE}(?, ?, ?, ?, ?, ?)}"
309
- result = cstmt_ord.call('hello', 10, [100, Fixnum], [Time.now, Time], Float, String)
308
+ cstmt_ord = conn.prepare_call "{call #{TEST_PROCEDURE}(?, ?, ?, ?, ?, ?, ?)}"
309
+ result = cstmt_ord.call('hello', 10, [100, Fixnum], [Time.now, Time], nil, Float, String)
310
310
  assert_instance_of Hash, result
311
311
  assert_equal 1000, result[3]
312
- assert_equal 'hello', result[6]
312
+ assert_equal 'hello', result[7]
313
313
 
314
314
  # Hash parameter
315
315
  cstmt_name = conn.prepare_call(case @type
316
316
  when :oracle
317
- "{call #{TEST_PROCEDURE}(:i1, :i2, :io1, :io2, :o1, :o2)}"
317
+ "{call #{TEST_PROCEDURE}(:i1, :i2, :io1, :io2, :n1, :o1, :o2)}"
318
318
  else
319
- "{call #{TEST_PROCEDURE}(?, ?, ?, ?, ?, ?)}"
319
+ "{call #{TEST_PROCEDURE}(?, ?, ?, ?, ?, ?, ?)}"
320
320
  end)
321
321
  result = cstmt_name.call(
322
322
  :i1 => 'hello', :i2 => 10,
323
323
  :io1 => [100, Fixnum], 'io2' => [Time.now, Time],
324
+ :n1 => nil,
324
325
  :o1 => Float, 'o2' => String)
325
326
  assert_instance_of Hash, result
326
327
  assert_equal 1000, result[:io1]
@@ -340,29 +341,40 @@ class TestConnection < Test::Unit::TestCase
340
341
  assert_raise(RuntimeError) { cstmt.call }
341
342
  end
342
343
 
343
- # pend('mysql raises data truncation error') do
344
+ # Data truncated for column 'io1' at row 2. WHY?
345
+ # http://www.herongyang.com/JDBC/MySQL-CallableStatement-INOUT-Parameters.html
344
346
  if @type != :mysql
345
- cstmt_ord = conn.prepare_call "{call #{TEST_PROCEDURE}(?, 10, ?, ?, ?, ?)}"
347
+ cstmt_ord = conn.prepare_call "{call #{TEST_PROCEDURE}('howdy', ?, ?, ?, ?, ?, ?)}"
346
348
  cstmt_name = conn.prepare_call(case @type
347
349
  when :oracle
348
- "{call #{TEST_PROCEDURE}(:i1, 10, :io1, :io2, :o1, :o2)}"
350
+ "{call #{TEST_PROCEDURE}('howdy', :i2, :io1, :io2, :n1, :o1, :o2)}"
349
351
  else
350
- "{call #{TEST_PROCEDURE}(?, 10, ?, ?, ?, ?)}"
352
+ "{call #{TEST_PROCEDURE}('howdy', ?, ?, ?, ?, ?, ?)}"
351
353
  end)
352
354
  # Hash parameter
353
355
  result = cstmt_name.call(
354
- :i1 => 'hello',# :i2 => 10,
356
+ #:i1 => 'hello',
357
+ :i2 => 10,
355
358
  :io1 => [100, Fixnum], 'io2' => [Time.now, Time],
359
+ :n1 => nil,
356
360
  :o1 => Float, 'o2' => String)
357
361
  assert_instance_of Hash, result
358
362
  assert_equal 1000, result[:io1]
359
- assert_equal 'hello', result['o2']
363
+ assert_equal 'howdy', result['o2']
360
364
 
361
365
  # Array parameter
362
- result = cstmt_ord.call('hello', [100, Fixnum], [Time.now, Time], Float, String)
366
+ result = cstmt_ord.call(10, [100, Fixnum], [Time.now, Time], nil, Float, String)
363
367
  assert_instance_of Hash, result
364
368
  assert_equal 1000, result[2]
365
- assert_equal 'hello', result[5]
369
+ assert_equal 'howdy', result[6]
370
+
371
+ # Close
372
+ [ cstmt_ord, cstmt_name ].each do | cstmt |
373
+ assert_equal false, cstmt.closed?
374
+ cstmt.close
375
+ assert_equal true, cstmt.closed?
376
+ assert_raise(RuntimeError) { cstmt.call }
377
+ end
366
378
  end
367
379
  end
368
380
  end
@@ -99,25 +99,58 @@ class TestObjectWrapper < Test::Unit::TestCase
99
99
  end
100
100
 
101
101
  def test_procedure_wrapper
102
- each_connection do |conn|
103
- create_test_procedure conn, @procedure_name
102
+ each_connection do |conn, conn_info|
103
+ {
104
+ :proc => @procedure_name,
105
+ :db_proc => [conn_info['database'], @procedure_name].join('.')
106
+ }.each do |mode, prname|
107
+ create_test_procedure_simple conn, prname
108
+
109
+ pr = conn.procedure(prname)
110
+ pr.call # should be ok without any arguments
104
111
 
105
- pr = conn.procedure(@procedure_name)
112
+ # Complex case
113
+ create_test_procedure conn, prname
114
+ pr.refresh
106
115
 
107
- result = pr.call 'hello', 10, [100, Fixnum], [Time.now, Time], Float, String
108
- assert_instance_of Hash, result
109
- assert_equal 1000, result[3]
110
- assert_equal 'hello', result[6]
116
+ result = pr.call 'hello', 10, [100, Fixnum], [Time.now, Time], nil, Float, String
117
+ assert_instance_of Hash, result
118
+ assert_equal 1000, result[3]
119
+ assert_equal 'hello', result[7]
111
120
 
112
- if @type != :oracle
113
121
  result = pr.call(
114
- :i1 => 'hello', :i2 => 10,
115
- :io1 => [100, Fixnum], 'io2' => [Time.now, Time],
122
+ :io1 => [100, Fixnum],
123
+ 'io2' => [Time.now, Time],
124
+ :i2 => 10,
125
+ :i1 => 'hello',
116
126
  :o1 => Float, 'o2' => String)
117
127
  assert_instance_of Hash, result
118
128
  assert_equal 1000, result[:io1]
119
129
  assert_equal 'hello', result['o2']
120
- end
130
+
131
+ # Test default values
132
+ # - MySQL does not support default values
133
+ # - Oracle JDBC does not fully implement getProcedureColumns
134
+ # => Cannot get default values with standard interface => Pending
135
+ if @type != :mysql
136
+ pend("Not tested") do
137
+ result = pr.call(
138
+ :io1 => [100, Fixnum],
139
+ 'io2' => [Time.now, Time],
140
+ #:i2 => 10,
141
+ :i1 => 'hello',
142
+ :o1 => Float, 'o2' => String)
143
+ assert_instance_of Hash, result
144
+ assert_equal 100, result[:io1]
145
+ assert_equal 'hello', result['o2']
146
+
147
+ result = pr.call 'hello', [100, Fixnum], [Time.now, Time], nil, Float, String
148
+ assert_instance_of Hash, result
149
+ assert_equal 100, result[3]
150
+ assert_equal 'hello', result[7]
151
+ end
152
+ end
153
+ end#prname
121
154
  end
122
155
  end
123
156
 
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: jdbc-helper
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.4.2
5
+ version: 0.4.3
6
6
  platform: ruby
7
7
  authors:
8
8
  - Junegunn Choi
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-06-04 00:00:00 +09:00
13
+ date: 2011-06-06 00:00:00 +09:00
14
14
  default_executable:
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
@@ -89,6 +89,7 @@ files:
89
89
  - LICENSE.txt
90
90
  - README.rdoc
91
91
  - test/database.yml
92
+ - test/database.yml.org
92
93
  - test/helper.rb
93
94
  - test/test_connection.rb
94
95
  - test/test_connectors.rb
@@ -128,6 +129,7 @@ specification_version: 3
128
129
  summary: A JDBC helper for JRuby/Database developers.
129
130
  test_files:
130
131
  - test/database.yml
132
+ - test/database.yml.org
131
133
  - test/helper.rb
132
134
  - test/test_connection.rb
133
135
  - test/test_connectors.rb