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.
@@ -1,5 +1,15 @@
1
+ begin
2
+ require "oci8"
3
+ rescue LoadError
4
+ # OCI8 driver is unavailable.
5
+ error_message = "ERROR: ruby-plsql could not load ruby-oci8 library. "+
6
+ "Please install ruby-oci8 gem."
7
+ STDERR.puts error_message
8
+ raise LoadError
9
+ end
10
+
1
11
  module PLSQL
2
- class OCIConnection < Connection
12
+ class OCIConnection < Connection #:nodoc:
3
13
 
4
14
  def logoff
5
15
  raw_connection.logoff
@@ -21,71 +31,80 @@ module PLSQL
21
31
  raw_connection.autocommit = value
22
32
  end
23
33
 
24
- def select_first(sql, *bindvars)
25
- cursor = raw_connection.exec(sql, *bindvars)
26
- result = cursor.fetch
27
- if result
28
- result.map { |val| ora_value_to_ruby_value(val) }
29
- else
30
- nil
31
- end
32
- ensure
33
- cursor.close rescue nil
34
- end
35
-
36
- def select_all(sql, *bindvars, &block)
37
- cursor = raw_connection.exec(sql, *bindvars)
38
- results = []
39
- row_count = 0
40
- while row = cursor.fetch
41
- row_with_typecast = row.map {|val| ora_value_to_ruby_value(val) }
42
- if block_given?
43
- yield(row_with_typecast)
44
- row_count += 1
45
- else
46
- results << row_with_typecast
47
- end
48
- end
49
- block_given? ? row_count : results
50
- ensure
51
- cursor.close rescue nil
52
- end
53
-
54
34
  def exec(sql, *bindvars)
55
35
  raw_connection.exec(sql, *bindvars)
56
36
  true
57
37
  end
58
38
 
59
- class Cursor
60
- attr_accessor :raw_cursor
61
-
62
- def initialize(raw_cur)
63
- @raw_cursor = raw_cur
39
+ class Cursor #:nodoc:
40
+ include Connection::CursorCommon
41
+
42
+ # stack of open cursors
43
+ @@open_cursors = []
44
+ attr_reader :raw_cursor
45
+
46
+ def initialize(conn, raw_cursor)
47
+ @connection = conn
48
+ @raw_cursor = raw_cursor
49
+ @@open_cursors.push self
64
50
  end
65
51
 
66
- def bind_param(key, value, type=nil, length=nil, in_out='IN')
67
- raw_cursor.bind_param(key, value, type, length)
52
+ def self.new_from_parse(conn, sql)
53
+ raw_cursor = conn.raw_connection.parse(sql)
54
+ self.new(conn, raw_cursor)
55
+ end
56
+
57
+ def self.new_from_query(conn, sql, *bindvars)
58
+ Cursor.new(conn, conn.raw_connection.exec(sql, *bindvars))
59
+ end
60
+
61
+ def bind_param(arg, value, metadata)
62
+ type, length = @connection.plsql_to_ruby_data_type(metadata)
63
+ ora_value = @connection.ruby_value_to_ora_value(value, type)
64
+ @raw_cursor.bind_param(arg, ora_value, type, length)
68
65
  end
69
66
 
70
67
  def exec(*bindvars)
71
- raw_cursor.exec(*bindvars)
68
+ @raw_cursor.exec(*bindvars)
72
69
  end
73
70
 
74
71
  def [](key)
75
- raw_cursor[key]
72
+ @connection.ora_value_to_ruby_value(@raw_cursor[key])
73
+ end
74
+
75
+ def fetch
76
+ row = @raw_cursor.fetch
77
+ row && row.map{|v| @connection.ora_value_to_ruby_value(v)}
78
+ end
79
+
80
+ def fields
81
+ @fields ||= @raw_cursor.get_col_names.map{|c| c.downcase.to_sym}
82
+ end
83
+
84
+ def close_raw_cursor
85
+ @raw_cursor.close
76
86
  end
77
87
 
78
88
  def close
79
- raw_cursor.close
89
+ # close all cursors that were created after this one
90
+ while (open_cursor = @@open_cursors.pop) && !open_cursor.equal?(self)
91
+ open_cursor.close_raw_cursor
92
+ end
93
+ close_raw_cursor
80
94
  end
95
+
81
96
  end
82
97
 
83
98
  def parse(sql)
84
- Cursor.new(raw_connection.parse(sql))
99
+ Cursor.new_from_parse(self, sql)
85
100
  end
86
-
87
101
 
88
- def plsql_to_ruby_data_type(data_type, data_length)
102
+ def cursor_from_query(sql, *bindvars)
103
+ Cursor.new_from_query(sql, *bindvars)
104
+ end
105
+
106
+ def plsql_to_ruby_data_type(metadata)
107
+ data_type, data_length = metadata[:data_type], metadata[:data_length]
89
108
  case data_type
90
109
  when "VARCHAR2"
91
110
  [String, data_length || 32767]
@@ -99,63 +118,125 @@ module PLSQL
99
118
  [DateTime, nil]
100
119
  when "TIMESTAMP"
101
120
  [Time, nil]
102
- # CLOB
103
- # BLOB
121
+ when "TABLE", "VARRAY", "OBJECT"
122
+ # create Ruby class for collection
123
+ klass = OCI8::Object::Base.get_class_by_typename(metadata[:sql_type_name])
124
+ unless klass
125
+ klass = Class.new(OCI8::Object::Base)
126
+ klass.set_typename metadata[:sql_type_name]
127
+ end
128
+ [klass, nil]
129
+ when "REF CURSOR"
130
+ [OCI8::Cursor]
104
131
  else
105
132
  [String, 32767]
106
133
  end
107
134
  end
108
135
 
109
- def ruby_value_to_ora_value(val, type)
110
- if type == OraNumber
136
+ def ruby_value_to_ora_value(value, type=nil)
137
+ type ||= value.class
138
+ case type.to_s.to_sym
139
+ when :Fixnum, :BigDecimal, :String
140
+ value
141
+ when :OraNumber
111
142
  # pass parameters as OraNumber to avoid rounding errors
112
- case val
143
+ case value
113
144
  when Bignum
114
- OraNumber.new(val.to_s)
145
+ OraNumber.new(value.to_s)
115
146
  when BigDecimal
116
- OraNumber.new(val.to_s('F'))
147
+ OraNumber.new(value.to_s('F'))
148
+ when TrueClass
149
+ OraNumber.new(1)
150
+ when FalseClass
151
+ OraNumber.new(0)
117
152
  else
118
- val
153
+ value
119
154
  end
120
- elsif type == DateTime
121
- case val
155
+ when :DateTime
156
+ case value
122
157
  when Time
123
- ::DateTime.civil(val.year, val.month, val.day, val.hour, val.min, val.sec, Rational(val.utc_offset, 86400))
158
+ ::DateTime.civil(value.year, value.month, value.day, value.hour, value.min, value.sec, Rational(value.utc_offset, 86400))
124
159
  when DateTime
125
- val
160
+ value
126
161
  when Date
127
- ::DateTime.civil(val.year, val.month, val.day, 0, 0, 0, 0)
162
+ ::DateTime.civil(value.year, value.month, value.day, 0, 0, 0, 0)
128
163
  else
129
- val
164
+ value
130
165
  end
131
- elsif type == OCI8::CLOB
132
- # ruby-oci8 cannot create CLOB from ''
133
- val = nil if val == ''
134
- OCI8::CLOB.new(raw_oci_connection, val)
135
- elsif type == OCI8::BLOB
136
- # ruby-oci8 cannot create BLOB from ''
137
- val = nil if val == ''
138
- OCI8::BLOB.new(raw_oci_connection, val)
166
+ when :"OCI8::CLOB", :"OCI8::BLOB"
167
+ # ruby-oci8 cannot create CLOB/BLOB from ''
168
+ value = nil if value == ''
169
+ type.new(raw_oci_connection, value)
170
+ when :"OCI8::Cursor"
171
+ value && value.raw_cursor
139
172
  else
140
- val
173
+ # collections and object types
174
+ if type.superclass == OCI8::Object::Base
175
+ return nil if value.nil?
176
+ tdo = raw_oci_connection.get_tdo_by_class(type)
177
+ if tdo.is_collection?
178
+ raise ArgumentError, "You should pass Array value for collection type parameter" unless value.is_a?(Array)
179
+ elem_list = value.map do |elem|
180
+ if (attr_tdo = tdo.coll_attr.typeinfo)
181
+ attr_type, attr_length = plsql_to_ruby_data_type(:data_type => 'OBJECT', :sql_type_name => attr_tdo.typename)
182
+ else
183
+ attr_type = elem.class
184
+ end
185
+ ruby_value_to_ora_value(elem, attr_type)
186
+ end
187
+ # construct collection value
188
+ # TODO: change setting instance variable to appropriate ruby-oci8 method call when available
189
+ collection = type.new(raw_oci_connection)
190
+ collection.instance_variable_set('@attributes', elem_list)
191
+ collection
192
+ else # object type
193
+ raise ArgumentError, "You should pass Hash value for object type parameter" unless value.is_a?(Hash)
194
+ object_attrs = value.dup
195
+ object_attrs.keys.each do |key|
196
+ raise ArgumentError, "Wrong object type field passed to PL/SQL procedure" unless (attr = tdo.attr_getters[key])
197
+ case attr.datatype
198
+ when OCI8::TDO::ATTR_NAMED_TYPE, OCI8::TDO::ATTR_NAMED_COLLECTION
199
+ # nested object type or collection
200
+ attr_type, attr_length = plsql_to_ruby_data_type(:data_type => 'OBJECT', :sql_type_name => attr.typeinfo.typename)
201
+ object_attrs[key] = ruby_value_to_ora_value(object_attrs[key], attr_type)
202
+ end
203
+ end
204
+ type.new(raw_oci_connection, object_attrs)
205
+ end
206
+ # all other cases
207
+ else
208
+ value
209
+ end
141
210
  end
142
211
  end
143
212
 
144
- def ora_value_to_ruby_value(val)
145
- case val
146
- when Float, OraNumber
147
- ora_number_to_ruby_number(val)
213
+ def ora_value_to_ruby_value(value)
214
+ case value
215
+ when Float, OraNumber, BigDecimal
216
+ ora_number_to_ruby_number(value)
148
217
  when DateTime, OraDate
149
- ora_date_to_ruby_date(val)
218
+ ora_date_to_ruby_date(value)
150
219
  when OCI8::LOB
151
- if val.available?
152
- val.rewind
153
- val.read
220
+ if value.available?
221
+ value.rewind
222
+ value.read
154
223
  else
155
224
  nil
156
225
  end
226
+ when OCI8::Object::Base
227
+ tdo = raw_oci_connection.get_tdo_by_class(value.class)
228
+ if tdo.is_collection?
229
+ value.to_ary.map{|e| ora_value_to_ruby_value(e)}
230
+ else # object type
231
+ tdo.attributes.inject({}) do |hash, attr|
232
+ hash[attr.name] = ora_value_to_ruby_value(value.instance_variable_get(:@attributes)[attr.name])
233
+ hash
234
+ end
235
+ end
236
+ when OCI8::Cursor
237
+ Cursor.new(self, value)
157
238
  else
158
- val
239
+ value
159
240
  end
160
241
  end
161
242
 
@@ -174,8 +255,7 @@ module PLSQL
174
255
 
175
256
  def ora_number_to_ruby_number(num)
176
257
  # return BigDecimal instead of Float to avoid rounding errors
177
- # num.to_i == num.to_f ? num.to_i : num.to_f
178
- num == (num_to_i = num.to_i) ? num_to_i : BigDecimal.new(num.to_s)
258
+ num == (num_to_i = num.to_i) ? num_to_i : (num.is_a?(BigDecimal) ? num : BigDecimal.new(num.to_s))
179
259
  end
180
260
 
181
261
  def ora_date_to_ruby_date(val)
@@ -1,6 +1,6 @@
1
1
  module PLSQL
2
2
 
3
- module PackageClassMethods
3
+ module PackageClassMethods #:nodoc:
4
4
  def find(schema, package)
5
5
  if schema.select_first("
6
6
  SELECT object_name FROM all_objects
@@ -27,7 +27,7 @@ module PLSQL
27
27
  end
28
28
  end
29
29
 
30
- class Package
30
+ class Package #:nodoc:
31
31
  extend PackageClassMethods
32
32
 
33
33
  def initialize(schema, package, override_schema_name = nil)
@@ -39,12 +39,12 @@ module PLSQL
39
39
 
40
40
  private
41
41
 
42
- def method_missing(method, *args)
42
+ def method_missing(method, *args, &block)
43
43
  if procedure = @procedures[method]
44
- procedure.exec(*args)
44
+ procedure.exec(*args, &block)
45
45
  elsif procedure = Procedure.find(@schema, method, @package, @override_schema_name)
46
46
  @procedures[method] = procedure
47
- procedure.exec(*args)
47
+ procedure.exec(*args, &block)
48
48
  else
49
49
  raise ArgumentError, "No PL/SQL procedure found"
50
50
  end
@@ -1,6 +1,6 @@
1
1
  module PLSQL
2
2
 
3
- module ProcedureClassMethods
3
+ module ProcedureClassMethods #:nodoc:
4
4
  def find(schema, procedure, package = nil, override_schema_name = nil)
5
5
  if package.nil?
6
6
  if schema.select_first("
@@ -38,9 +38,12 @@ module PLSQL
38
38
  end
39
39
  end
40
40
 
41
- class Procedure
41
+ class Procedure #:nodoc:
42
42
  extend ProcedureClassMethods
43
43
 
44
+ attr_reader :arguments, :argument_list, :out_list, :return
45
+ attr_reader :schema, :schema_name, :package, :procedure
46
+
44
47
  def initialize(schema, procedure, package = nil, override_schema_name = nil)
45
48
  @schema = schema
46
49
  @schema_name = override_schema_name || schema.schema_name
@@ -51,6 +54,10 @@ module PLSQL
51
54
  @out_list = {}
52
55
  @return = {}
53
56
  @overloaded = false
57
+
58
+ # store reference to previous level record or collection metadata
59
+ previous_level_argument_metadata = {}
60
+
54
61
  # RSI: due to 10gR2 all_arguments performance issue SELECT split into two statements
55
62
  # added condition to ensure that if object is package then package specification not body is selected
56
63
  object_id = @schema.connection.select_first("
@@ -62,41 +69,68 @@ module PLSQL
62
69
  ", @schema_name, @package ? @package : @procedure
63
70
  )[0] rescue nil
64
71
  num_rows = @schema.connection.select_all("
65
- SELECT a.argument_name, a.position, a.data_type, a.in_out, a.data_length, a.data_precision, a.data_scale, a.overload
66
- FROM all_arguments a
67
- WHERE a.object_id = :object_id
68
- AND a.owner = :owner
69
- AND a.object_name = :procedure_name
70
- AND NVL(a.package_name,'nil') = :package
72
+ SELECT overload, argument_name, position, data_level,
73
+ data_type, in_out, data_length, data_precision, data_scale, char_used,
74
+ type_owner, type_name, type_subname
75
+ FROM all_arguments
76
+ WHERE object_id = :object_id
77
+ AND owner = :owner
78
+ AND object_name = :procedure_name
79
+ AND NVL(package_name,'nil') = :package
80
+ ORDER BY overload, sequence
71
81
  ", object_id, @schema_name, @procedure, @package ? @package : 'nil'
72
82
  ) do |r|
73
83
 
74
- argument_name, position, data_type, in_out, data_length, data_precision, data_scale, overload = r
84
+ overload, argument_name, position, data_level,
85
+ data_type, in_out, data_length, data_precision, data_scale, char_used,
86
+ type_owner, type_name, type_subname = r
75
87
 
76
88
  @overloaded ||= !overload.nil?
77
89
  # if not overloaded then store arguments at key 0
78
90
  overload ||= 0
79
91
  @arguments[overload] ||= {}
80
92
  @return[overload] ||= nil
81
-
93
+
94
+ raise ArgumentError, "Parameter type definition inside package is not supported, use CREATE TYPE outside package" if type_subname
95
+
96
+ argument_metadata = {
97
+ :position => position && position.to_i,
98
+ :data_type => data_type,
99
+ :in_out => in_out,
100
+ :data_length => data_length && data_length.to_i,
101
+ :data_precision => data_precision && data_precision.to_i,
102
+ :data_scale => data_scale && data_scale.to_i,
103
+ :char_used => char_used,
104
+ :type_owner => type_owner,
105
+ :type_name => type_name,
106
+ :type_subname => type_subname,
107
+ :sql_type_name => "#{type_owner}.#{type_name}"
108
+ }
109
+ if composite_type?(data_type)
110
+ case data_type
111
+ when 'PL/SQL RECORD'
112
+ argument_metadata[:fields] = {}
113
+ end
114
+ previous_level_argument_metadata[data_level] = argument_metadata
115
+ end
116
+
117
+ # if parameter
82
118
  if argument_name
83
- @arguments[overload][argument_name.downcase.to_sym] = {
84
- :position => position,
85
- :data_type => data_type,
86
- :in_out => in_out,
87
- :data_length => data_length,
88
- :data_precision => data_precision,
89
- :data_scale => data_scale
90
- }
119
+ # top level parameter
120
+ if data_level == 0
121
+ @arguments[overload][argument_name.downcase.to_sym] = argument_metadata
122
+ # or lower level part of composite type
123
+ else
124
+ case previous_level_argument_metadata[data_level - 1][:data_type]
125
+ when 'PL/SQL RECORD'
126
+ previous_level_argument_metadata[data_level - 1][:fields][argument_name.downcase.to_sym] = argument_metadata
127
+ when 'TABLE', 'VARRAY'
128
+ previous_level_argument_metadata[data_level - 1][:element] = argument_metadata
129
+ end
130
+ end
91
131
  # if function has return value
92
- elsif position == 0 && in_out == 'OUT'
93
- @return[overload] = {
94
- :data_type => data_type,
95
- :in_out => in_out,
96
- :data_length => data_length,
97
- :data_precision => data_precision,
98
- :data_scale => data_scale
99
- }
132
+ elsif argument_name.nil? && data_level == 0 && in_out == 'OUT'
133
+ @return[overload] = argument_metadata
100
134
  end
101
135
  end
102
136
  # if procedure is without arguments then create default empty argument list for default overload
@@ -108,118 +142,19 @@ module PLSQL
108
142
  @out_list[overload] = @argument_list[overload].select {|k| @arguments[overload][k][:in_out] =~ /OUT/}
109
143
  end
110
144
  end
111
-
145
+
146
+ PLSQL_COMPOSITE_TYPES = ['PL/SQL RECORD', 'TABLE', 'VARRAY'].freeze
147
+ def composite_type?(data_type)
148
+ PLSQL_COMPOSITE_TYPES.include? data_type
149
+ end
150
+
112
151
  def overloaded?
113
152
  @overloaded
114
153
  end
115
154
 
116
- def exec(*args)
117
- # find which overloaded definition to use
118
- # if definition is overloaded then match by number of arguments
119
- if @overloaded
120
- # named arguments
121
- if args.size == 1 && args[0].is_a?(Hash)
122
- number_of_args = args[0].keys.size
123
- overload = @argument_list.keys.detect do |ov|
124
- @argument_list[ov].size == number_of_args &&
125
- @arguments[ov].keys.sort_by{|k| k.to_s} == args[0].keys.sort_by{|k| k.to_s}
126
- end
127
- # sequential arguments
128
- # TODO: should try to implement matching by types of arguments
129
- else
130
- number_of_args = args.size
131
- overload = @argument_list.keys.detect do |ov|
132
- @argument_list[ov].size == number_of_args
133
- end
134
- end
135
- raise ArgumentError, "Wrong number of arguments passed to overloaded PL/SQL procedure" unless overload
136
- else
137
- overload = 0
138
- end
139
-
140
- sql = "BEGIN\n"
141
- sql << ":return := " if @return[overload]
142
- sql << "#{@schema_name}." if @schema_name
143
- sql << "#{@package}." if @package
144
- sql << "#{@procedure}("
145
-
146
- # Named arguments
147
- args_list = []
148
- args_hash = {}
149
- if args.size == 1 and args[0].is_a?(Hash)
150
- sql << args[0].map do |k,v|
151
- raise ArgumentError, "Wrong argument passed to PL/SQL procedure" unless @arguments[overload][k]
152
- args_list << k
153
- args_hash[k] = v
154
- "#{k.to_s} => :#{k.to_s}"
155
- end.join(', ')
156
- # Sequential arguments
157
- else
158
- raise ArgumentError, "Too many arguments passed to PL/SQL procedure" if args.size > @argument_list[overload].size
159
- # Add missing arguments with nil value
160
- args = args + [nil]*(@argument_list[overload].size-args.size) if args.size < @argument_list[overload].size
161
- i = 0
162
- sql << args.map do |v|
163
- k = @argument_list[overload][i]
164
- i += 1
165
- args_list << k
166
- args_hash[k] = v
167
- ":#{k.to_s}"
168
- end.join(', ')
169
- end
170
- sql << ");\n"
171
- sql << "END;\n"
172
-
173
- cursor = @schema.connection.parse(sql)
174
-
175
- args_list.each do |k|
176
- data_type, data_length = plsql_to_ruby_data_type(@arguments[overload][k])
177
- cursor.bind_param(":#{k.to_s}", ruby_value_to_ora_value(args_hash[k], data_type),
178
- data_type, data_length, @arguments[overload][k][:in_out])
179
- end
180
-
181
- if @return[overload]
182
- data_type, data_length = plsql_to_ruby_data_type(@return[overload])
183
- cursor.bind_param(":return", nil, data_type, data_length, 'OUT')
184
- end
185
-
186
- cursor.exec
187
-
188
- # if function with output parameters
189
- if @return[overload] && @out_list[overload].size > 0
190
- result = [ora_value_to_ruby_value(cursor[':return']), {}]
191
- @out_list[overload].each do |k|
192
- result[1][k] = ora_value_to_ruby_value(cursor[":#{k}"])
193
- end
194
- # if function without output parameters
195
- elsif @return[overload]
196
- result = ora_value_to_ruby_value(cursor[':return'])
197
- # if procedure with output parameters
198
- elsif @out_list[overload].size > 0
199
- result = {}
200
- @out_list[overload].each do |k|
201
- result[k] = ora_value_to_ruby_value(cursor[":#{k}"])
202
- end
203
- # if procedure without output parameters
204
- else
205
- result = nil
206
- end
207
- cursor.close
208
- result
209
- end
210
-
211
- private
212
-
213
- def plsql_to_ruby_data_type(argument)
214
- @schema.connection.plsql_to_ruby_data_type(argument[:data_type],argument[:data_length])
215
- end
216
-
217
- def ruby_value_to_ora_value(val, type)
218
- @schema.connection.ruby_value_to_ora_value(val, type)
219
- end
220
-
221
- def ora_value_to_ruby_value(val)
222
- @schema.connection.ora_value_to_ruby_value(val)
155
+ def exec(*args, &block)
156
+ call = ProcedureCall.new(self, args)
157
+ call.exec(&block)
223
158
  end
224
159
 
225
160
  end