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