jdbc-helper 0.4.2 → 0.4.3

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