ruby-plsql 0.4.1 → 0.4.2

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/History.txt CHANGED
@@ -1,3 +1,21 @@
1
+ == 0.4.2 2010-02-26
2
+
3
+ * New features
4
+ * Support default and custom constructors of object types, support member and static method calls on PL/SQL objects
5
+ * Support for PL/SQL record types defined inside packages
6
+ * Support for PL/SQL table and index-by table of records types defined inside packages
7
+ * plsql.savepoint and plsql.rollback_to methods
8
+ * plsql.connect! method for establishing new connection
9
+ * Improvements
10
+ * Better support for detecting matching overloaded implementation of procedure by sequential argument types
11
+ * Check if database object is valid and raise exception with compilation error if not valid
12
+ * Store :nullable and :data_default in table and view columns metadata
13
+ * Bug fixes
14
+ * accessing package variables with schema prefixed object types
15
+ * insert of TIMESTAMP values in table
16
+ * support package variables with VARCHAR2(n CHAR) and VARCHAR2(n BYTE) types
17
+ * table select :order_by option
18
+
1
19
  == 0.4.1 2010-01-04
2
20
 
3
21
  * New features
data/Rakefile CHANGED
@@ -15,6 +15,8 @@ EOS
15
15
  gem.homepage = "http://github.com/rsim/ruby-plsql"
16
16
  gem.authors = ["Raimonds Simanovskis"]
17
17
  gem.add_development_dependency "rspec", ">= 1.2.9"
18
+ gem.add_development_dependency "activerecord", "= 2.3.5"
19
+ gem.add_development_dependency "activerecord-oracle_enhanced-adapter", ">= 1.2.4"
18
20
  gem.extra_rdoc_files = ['README.rdoc']
19
21
  end
20
22
  Jeweler::GemcutterTasks.new
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.1
1
+ 0.4.2
@@ -3,27 +3,49 @@ 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) #:nodoc:
7
- @raw_driver = raw_drv
6
+ def initialize(raw_conn, ar_class = nil) #:nodoc:
7
+ @raw_driver = self.class.driver_type
8
8
  @raw_connection = raw_conn
9
9
  @activerecord_class = ar_class
10
10
  end
11
-
11
+
12
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
16
+ case driver_type
17
+ when :oci
18
+ OCIConnection.new(raw_conn, ar_class)
19
+ when :jdbc
20
+ JDBCConnection.new(raw_conn, ar_class)
21
+ else
22
+ raise ArgumentError, "Unknown raw driver"
23
+ end
24
+ end
25
+
26
+ def self.create_new(params) #:nodoc:
27
+ case driver_type
28
+ when :oci
29
+ OCIConnection.create_raw(params)
30
+ when :jdbc
31
+ JDBCConnection.create_raw(params)
32
+ else
33
+ raise ArgumentError, "Unknown raw driver"
34
+ end
35
+ end
36
+
37
+ def self.driver_type #:nodoc:
16
38
  # MRI 1.8.6 or YARV 1.9.1
17
- if (!defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby") && defined?(OCI8)
18
- OCIConnection.new(:oci, raw_conn, ar_class)
39
+ @driver_type ||= if (!defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby") && defined?(OCI8)
40
+ :oci
19
41
  # JRuby
20
42
  elsif (defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby")
21
- JDBCConnection.new(:jdbc, raw_conn, ar_class)
43
+ :jdbc
22
44
  else
23
- raise ArgumentError, "Unknown raw driver"
45
+ nil
24
46
  end
25
47
  end
26
-
48
+
27
49
  # Returns OCI8 or JDBC connection
28
50
  def raw_connection
29
51
  if @activerecord_class
@@ -32,7 +54,7 @@ module PLSQL
32
54
  @raw_connection
33
55
  end
34
56
  end
35
-
57
+
36
58
  # Is it OCI8 connection
37
59
  def oci?
38
60
  @raw_driver == :oci
@@ -44,7 +66,10 @@ module PLSQL
44
66
  end
45
67
 
46
68
  def logoff #:nodoc:
47
- raise NoMethodError, "Not implemented for this raw driver"
69
+ # Rollback any uncommited transactions
70
+ rollback
71
+ # Common cleanup activities before logoff, should be called from particular driver method
72
+ drop_session_ruby_temporary_tables
48
73
  end
49
74
 
50
75
  def commit #:nodoc:
@@ -165,6 +190,31 @@ module PLSQL
165
190
  raise NoMethodError, "Not implemented for this raw driver"
166
191
  end
167
192
 
193
+ # Returns session ID
194
+ def session_id
195
+ @session_id ||= select_first("SELECT TO_NUMBER(USERENV('SESSIONID')) FROM dual")[0]
196
+ end
197
+
198
+ RUBY_TEMP_TABLE_PREFIX = 'ruby_'
199
+
200
+ # Drop all ruby temporary tables that are used for calling packages with table parameter types defined in packages
201
+ def drop_all_ruby_temporary_tables
202
+ select_all("SELECT table_name FROM user_tables WHERE temporary='Y' AND table_name LIKE :table_name",
203
+ RUBY_TEMP_TABLE_PREFIX.upcase+'%').each do |row|
204
+ exec "TRUNCATE TABLE #{row[0]}"
205
+ exec "DROP TABLE #{row[0]}"
206
+ end
207
+ end
208
+
209
+ # Drop ruby temporary tables created in current session that are used for calling packages with table parameter types defined in packages
210
+ def drop_session_ruby_temporary_tables
211
+ select_all("SELECT table_name FROM user_tables WHERE temporary='Y' AND table_name LIKE :table_name",
212
+ RUBY_TEMP_TABLE_PREFIX.upcase+"#{session_id}_%").each do |row|
213
+ exec "TRUNCATE TABLE #{row[0]}"
214
+ exec "DROP TABLE #{row[0]}"
215
+ end
216
+ end
217
+
168
218
  end
169
219
 
170
220
  end
@@ -31,7 +31,18 @@ end
31
31
 
32
32
  module PLSQL
33
33
  class JDBCConnection < Connection #:nodoc:
34
+
35
+ def self.create_raw(params)
36
+ url = if ENV['TNS_ADMIN'] && params[:database] && !params[:host] && !params[:url]
37
+ "jdbc:oracle:thin:@#{params[:database]}"
38
+ else
39
+ params[:url] || "jdbc:oracle:thin:@#{params[:host] || 'localhost'}:#{params[:port] || 1521}:#{params[:database]}"
40
+ end
41
+ new(java.sql.DriverManager.getConnection(url, params[:username], params[:password]))
42
+ end
43
+
34
44
  def logoff
45
+ super
35
46
  raw_connection.close
36
47
  true
37
48
  rescue
@@ -84,7 +95,7 @@ module PLSQL
84
95
  if metadata[:in_out] =~ /OUT/
85
96
  @out_types[arg] = type || ora_value.class
86
97
  @out_index[arg] = bind_param_index(arg)
87
- if ['TABLE','VARRAY','OBJECT','REF CURSOR'].include?(metadata[:data_type])
98
+ if ['TABLE','VARRAY','OBJECT'].include?(metadata[:data_type])
88
99
  @statement.registerOutParameter(@out_index[arg], @connection.get_java_sql_type(ora_value,type),
89
100
  metadata[:sql_type_name])
90
101
  else
@@ -209,10 +220,11 @@ module PLSQL
209
220
  Hash => Java::oracle.jdbc.OracleTypes::STRUCT,
210
221
  java.sql.ResultSet => Java::oracle.jdbc.OracleTypes::CURSOR,
211
222
  }
212
-
223
+
213
224
  SQL_TYPE_TO_RUBY_CLASS = {
214
225
  java.sql.Types::CHAR => String,
215
226
  java.sql.Types::VARCHAR => String,
227
+ java.sql.Types::LONGVARCHAR => String,
216
228
  java.sql.Types::NUMERIC => BigDecimal,
217
229
  java.sql.Types::INTEGER => Fixnum,
218
230
  java.sql.Types::DATE => Time,
@@ -16,8 +16,18 @@ end
16
16
 
17
17
  module PLSQL
18
18
  class OCIConnection < Connection #:nodoc:
19
-
19
+
20
+ def self.create_raw(params)
21
+ connection_string = if params[:host]
22
+ "//#{params[:host]}:#{params[:port]||1521}/#{params[:database]}"
23
+ else
24
+ params[:database]
25
+ end
26
+ new(OCI8.new(params[:username], params[:password], connection_string))
27
+ end
28
+
20
29
  def logoff
30
+ super
21
31
  raw_connection.logoff
22
32
  end
23
33
 
@@ -42,17 +42,31 @@ module PLSQL
42
42
  end
43
43
  end
44
44
 
45
- class Procedure #:nodoc:
46
- extend ProcedureClassMethods
47
-
45
+ module ProcedureCommon #:nodoc:
48
46
  attr_reader :arguments, :argument_list, :out_list, :return
49
47
  attr_reader :schema, :schema_name, :package, :procedure
50
48
 
51
- def initialize(schema, procedure, package, override_schema_name, object_id)
52
- @schema = schema
53
- @schema_name = override_schema_name || schema.schema_name
54
- @procedure = procedure.to_s.upcase
55
- @package = package
49
+ # return type string from metadata that can be used in DECLARE block or table definition
50
+ def self.type_to_sql(metadata) #:nodoc:
51
+ case metadata[:data_type]
52
+ when 'NUMBER'
53
+ precision, scale = metadata[:data_precision], metadata[:data_scale]
54
+ "NUMBER#{precision ? "(#{precision}#{scale ? ",#{scale}": ""})" : ""}"
55
+ when 'VARCHAR2', 'CHAR', 'NVARCHAR2', 'NCHAR'
56
+ length = metadata[:data_length]
57
+ if length && (char_used = metadata[:char_used])
58
+ length = "#{length} #{char_used == 'C' ? 'CHAR' : 'BYTE'}"
59
+ end
60
+ "#{metadata[:data_type]}#{length ? "(#{length})": ""}"
61
+ when 'PL/SQL TABLE', 'TABLE', 'VARRAY', 'OBJECT'
62
+ metadata[:sql_type_name]
63
+ else
64
+ metadata[:data_type]
65
+ end
66
+ end
67
+
68
+ # get procedure argument metadata from data dictionary
69
+ def get_argument_metadata #:nodoc:
56
70
  @arguments = {}
57
71
  @argument_list = {}
58
72
  @out_list = {}
@@ -62,20 +76,27 @@ module PLSQL
62
76
  # store reference to previous level record or collection metadata
63
77
  previous_level_argument_metadata = {}
64
78
 
79
+ # store tmp tables for each overload for table parameters with types defined inside packages
80
+ @tmp_table_names = {}
81
+ # store if tmp tables are created for specific overload
82
+ @tmp_tables_created = {}
83
+
84
+ # subprogram_id column is available just from version 10g
85
+ subprogram_id_column = (@schema.connection.database_version <=> [10, 2]) >= 0 ? 'subprogram_id' : 'NULL'
86
+
65
87
  @schema.select_all(
66
- "SELECT overload, argument_name, position, data_level,
88
+ "SELECT #{subprogram_id_column}, object_name, TO_NUMBER(overload), argument_name, position, data_level,
67
89
  data_type, in_out, data_length, data_precision, data_scale, char_used,
68
90
  type_owner, type_name, type_subname
69
91
  FROM all_arguments
70
92
  WHERE object_id = :object_id
71
93
  AND owner = :owner
72
94
  AND object_name = :procedure_name
73
- AND NVL(package_name,'nil') = :package
74
95
  ORDER BY overload, sequence",
75
- object_id, @schema_name, @procedure, @package ? @package : 'nil'
96
+ @object_id, @schema_name, @procedure
76
97
  ) do |r|
77
98
 
78
- overload, argument_name, position, data_level,
99
+ subprogram_id, object_name, overload, argument_name, position, data_level,
79
100
  data_type, in_out, data_length, data_precision, data_scale, char_used,
80
101
  type_owner, type_name, type_subname = r
81
102
 
@@ -84,8 +105,26 @@ module PLSQL
84
105
  overload ||= 0
85
106
  @arguments[overload] ||= {}
86
107
  @return[overload] ||= nil
87
-
88
- raise ArgumentError, "Parameter type definition inside package is not supported, use CREATE TYPE outside package" if type_subname
108
+ @tmp_table_names[overload] ||= []
109
+
110
+ sql_type_name = type_owner && "#{type_owner == 'PUBLIC' ? nil : "#{type_owner}."}#{type_name}#{type_subname ? ".#{type_subname}" : nil}"
111
+
112
+ tmp_table_name = nil
113
+ # type defined inside package
114
+ if type_subname
115
+ if collection_type?(data_type)
116
+ raise ArgumentError, "#{data_type} type #{sql_type_name} definition inside package is not supported as part of other type definition," <<
117
+ " use CREATE TYPE outside package" if data_level > 0
118
+ # if subprogram_id was not supported by all_arguments view
119
+ # then generate unique ID from object_name and overload
120
+ subprogram_id ||= "#{object_name.hash % 10000}#{overload}"
121
+ tmp_table_name = "#{Connection::RUBY_TEMP_TABLE_PREFIX}#{@schema.connection.session_id}_#{@object_id}_#{subprogram_id}_#{position}"
122
+ elsif data_type != 'PL/SQL RECORD'
123
+ # raise exception only when there are no overloaded procedure definitions
124
+ # (as probably this overload will not be used at all)
125
+ raise ArgumentError, "Parameter type #{sql_type_name} definition inside package is not supported, use CREATE TYPE outside package" if overload == 0
126
+ end
127
+ end
89
128
 
90
129
  argument_metadata = {
91
130
  :position => position && position.to_i,
@@ -98,8 +137,12 @@ module PLSQL
98
137
  :type_owner => type_owner,
99
138
  :type_name => type_name,
100
139
  :type_subname => type_subname,
101
- :sql_type_name => "#{type_owner}.#{type_name}"
140
+ :sql_type_name => sql_type_name
102
141
  }
142
+ if tmp_table_name
143
+ @tmp_table_names[overload] << [(argument_metadata[:tmp_table_name] = tmp_table_name), argument_metadata]
144
+ end
145
+
103
146
  if composite_type?(data_type)
104
147
  case data_type
105
148
  when 'PL/SQL RECORD'
@@ -108,28 +151,33 @@ module PLSQL
108
151
  previous_level_argument_metadata[data_level] = argument_metadata
109
152
  end
110
153
 
154
+ # if function has return value
155
+ if argument_name.nil? && data_level == 0 && in_out == 'OUT'
156
+ @return[overload] = argument_metadata
111
157
  # if parameter
112
- if argument_name
158
+ else
113
159
  # top level parameter
114
160
  if data_level == 0
115
- @arguments[overload][argument_name.downcase.to_sym] = argument_metadata
161
+ # sometime there are empty IN arguments in all_arguments view for procedures without arguments (e.g. for DBMS_OUTPUT.DISABLE)
162
+ @arguments[overload][argument_name.downcase.to_sym] = argument_metadata if argument_name
116
163
  # or lower level part of composite type
117
164
  else
118
165
  case previous_level_argument_metadata[data_level - 1][:data_type]
119
166
  when 'PL/SQL RECORD'
120
167
  previous_level_argument_metadata[data_level - 1][:fields][argument_name.downcase.to_sym] = argument_metadata
121
- when 'TABLE', 'VARRAY'
168
+ when 'PL/SQL TABLE', 'TABLE', 'VARRAY'
122
169
  previous_level_argument_metadata[data_level - 1][:element] = argument_metadata
123
170
  end
124
171
  end
125
- # if function has return value
126
- elsif argument_name.nil? && data_level == 0 && in_out == 'OUT'
127
- @return[overload] = argument_metadata
128
172
  end
129
173
  end
130
174
  # if procedure is without arguments then create default empty argument list for default overload
131
175
  @arguments[0] = {} if @arguments.keys.empty?
132
-
176
+
177
+ construct_argument_list_for_overloads
178
+ end
179
+
180
+ def construct_argument_list_for_overloads #:nodoc:
133
181
  @overloads = @arguments.keys.sort
134
182
  @overloads.each do |overload|
135
183
  @argument_list[overload] = @arguments[overload].keys.sort {|k1, k2| @arguments[overload][k1][:position] <=> @arguments[overload][k2][:position]}
@@ -137,14 +185,61 @@ module PLSQL
137
185
  end
138
186
  end
139
187
 
140
- PLSQL_COMPOSITE_TYPES = ['PL/SQL RECORD', 'TABLE', 'VARRAY'].freeze
141
- def composite_type?(data_type)
188
+ def ensure_tmp_tables_created(overload) #:nodoc:
189
+ return if @tmp_tables_created.nil? || @tmp_tables_created[overload]
190
+ @tmp_table_names[overload] && @tmp_table_names[overload].each do |table_name, argument_metadata|
191
+ sql = "CREATE GLOBAL TEMPORARY TABLE #{table_name} (\n"
192
+ element_metadata = argument_metadata[:element]
193
+ case element_metadata[:data_type]
194
+ when 'PL/SQL RECORD'
195
+ fields_metadata = element_metadata[:fields]
196
+ fields_sorted_by_position = fields_metadata.keys.sort_by{|k| fields_metadata[k][:position]}
197
+ sql << fields_sorted_by_position.map do |field|
198
+ metadata = fields_metadata[field]
199
+ "#{field} #{ProcedureCommon.type_to_sql(metadata)}"
200
+ end.join(",\n")
201
+ else
202
+ sql << "element #{ProcedureCommon.type_to_sql(element_metadata)}"
203
+ end
204
+ sql << ",\ni__ NUMBER(38)\n"
205
+ sql << ") ON COMMIT PRESERVE ROWS\n"
206
+ sql_block = "DECLARE\nPRAGMA AUTONOMOUS_TRANSACTION;\nBEGIN\nEXECUTE IMMEDIATE :sql;\nEND;\n"
207
+ @schema.execute sql_block, sql
208
+ end
209
+ @tmp_tables_created[overload] = true
210
+ end
211
+
212
+ PLSQL_COMPOSITE_TYPES = ['PL/SQL RECORD', 'PL/SQL TABLE', 'TABLE', 'VARRAY'].freeze
213
+ def composite_type?(data_type) #:nodoc:
142
214
  PLSQL_COMPOSITE_TYPES.include? data_type
143
215
  end
144
216
 
145
- def overloaded?
217
+ PLSQL_COLLECTION_TYPES = ['PL/SQL TABLE', 'TABLE', 'VARRAY'].freeze
218
+ def collection_type?(data_type) #:nodoc:
219
+ PLSQL_COLLECTION_TYPES.include? data_type
220
+ end
221
+
222
+ def overloaded? #:nodoc:
146
223
  @overloaded
147
224
  end
225
+ end
226
+
227
+ class Procedure #:nodoc:
228
+ extend ProcedureClassMethods
229
+ include ProcedureCommon
230
+
231
+ attr_reader :arguments, :argument_list, :out_list, :return
232
+ attr_reader :schema, :schema_name, :package, :procedure
233
+
234
+ def initialize(schema, procedure, package, override_schema_name, object_id)
235
+ @schema = schema
236
+ @schema_name = override_schema_name || schema.schema_name
237
+ @procedure = procedure.to_s.upcase
238
+ @package = package
239
+ @object_id = object_id
240
+
241
+ get_argument_metadata
242
+ end
148
243
 
149
244
  def exec(*args, &block)
150
245
  call = ProcedureCall.new(self, args)
@@ -1,15 +1,19 @@
1
1
  module PLSQL
2
2
  class ProcedureCall #:nodoc:
3
3
 
4
- def initialize(procedure, args = [])
4
+ def initialize(procedure, args = [], options = {})
5
5
  @procedure = procedure
6
6
  @schema = @procedure.schema
7
7
  @dbms_output_stream = @schema.dbms_output_stream
8
+ @skip_self = options[:skip_self]
9
+ @self = options[:self]
8
10
  @overload = get_overload_from_arguments_list(args)
11
+ @procedure.ensure_tmp_tables_created(@overload) if @procedure.respond_to?(:ensure_tmp_tables_created)
9
12
  construct_sql(args)
10
13
  end
11
14
 
12
15
  def exec
16
+ # puts "DEBUG: sql = #{@sql.gsub("\n","<br/>\n")}"
13
17
  @cursor = @schema.connection.parse(@sql)
14
18
 
15
19
  @bind_values.each do |arg, value|
@@ -37,34 +41,106 @@ module PLSQL
37
41
  private
38
42
 
39
43
  def get_overload_from_arguments_list(args)
40
- # find which overloaded definition to use
41
- # if definition is overloaded then match by number of arguments
42
- if @procedure.overloaded?
43
- # named arguments
44
- if args.size == 1 && args[0].is_a?(Hash)
45
- number_of_args = args[0].keys.size
46
- overload = overload_argument_list.keys.detect do |ov|
47
- overload_argument_list[ov].size == number_of_args &&
48
- overload_arguments[ov].keys.sort_by{|k| k.to_s} == args[0].keys.sort_by{|k| k.to_s}
49
- end
50
- # sequential arguments
51
- # TODO: should try to implement matching by types of arguments
52
- else
53
- number_of_args = args.size
54
- overload = overload_argument_list.keys.detect do |ov|
55
- overload_argument_list[ov].size == number_of_args
56
- end
44
+ # if not overloaded then overload index 0 is used
45
+ return 0 unless @procedure.overloaded?
46
+ # If named arguments are used then
47
+ # there should be just one Hash argument with symbol keys
48
+ if args.size == 1 && args[0].is_a?(Hash) && args[0].keys.all?{|k| k.is_a?(Symbol)}
49
+ args_keys = args[0].keys
50
+ # implicit SELF argument for object instance procedures
51
+ args_keys << :self if @self && !args_keys.include?(:self)
52
+ args_keys = args_keys.sort_by{|k| k.to_s}
53
+ number_of_args = args_keys.size
54
+ overload = overload_argument_list.keys.detect do |ov|
55
+ overload_argument_list[ov].size == number_of_args &&
56
+ overload_arguments[ov].keys.sort_by{|k| k.to_s} == args_keys
57
57
  end
58
- raise ArgumentError, "Wrong number of arguments passed to overloaded PL/SQL procedure" unless overload
59
- overload
58
+ # otherwise try matching by sequential arguments count and types
60
59
  else
61
- 0
60
+ number_of_args = args.size
61
+ matching_types = []
62
+ # if implicit SELF argument for object instance procedures should be passed
63
+ # then it should be added as first argument to find matches
64
+ if @self
65
+ number_of_args += 1
66
+ matching_types << ['OBJECT']
67
+ end
68
+ args.each do |arg|
69
+ matching_types << matching_oracle_types_for_ruby_value(arg)
70
+ end
71
+ exact_overloads = [] # overloads with exact number of matching arguments
72
+ smaller_overloads = [] # overloads with exact number of matching arguments
73
+ # overload = overload_argument_list.keys.detect do |ov|
74
+ # overload_argument_list[ov].size == number_of_args
75
+ # end
76
+ overload_argument_list.keys.each do |ov|
77
+ score = 0 # lower score is better match
78
+ ov_arg_list_size = overload_argument_list[ov].size
79
+ if (number_of_args <= ov_arg_list_size &&
80
+ (0..(number_of_args-1)).all? do |i|
81
+ ov_arg = overload_argument_list[ov][i]
82
+ matching_types[i] == :all || # either value matches any type
83
+ (ind = matching_types[i].index(overload_arguments[ov][ov_arg][:data_type])) &&
84
+ (score += ind) # or add index of matched type
85
+ end)
86
+ if number_of_args == ov_arg_list_size
87
+ exact_overloads << [ov, score]
88
+ else
89
+ smaller_overloads << [ov, score]
90
+ end
91
+ end
92
+ end
93
+ # pick either first exact matching overload of first matching with smaller argument count
94
+ # (hoping that missing arguments will be defaulted - cannot find default value from all_arguments)
95
+ overload = if !exact_overloads.empty?
96
+ exact_overloads.sort_by{|ov, score| score}[0][0]
97
+ elsif !smaller_overloads.empty?
98
+ smaller_overloads.sort_by{|ov, score| score}[0][0]
99
+ end
100
+ end
101
+ raise ArgumentError, "Wrong number or types of arguments passed to overloaded PL/SQL procedure" unless overload
102
+ overload
103
+ end
104
+
105
+ MATCHING_TYPES = {
106
+ :integer => ['NUMBER', 'PLS_INTEGER', 'BINARY_INTEGER'],
107
+ :decimal => ['NUMBER', 'BINARY_FLOAT', 'BINARY_DOUBLE'],
108
+ :string => ['VARCHAR2', 'NVARCHAR2', 'CHAR', 'NCHAR', 'CLOB', 'BLOB'],
109
+ :date => ['DATE'],
110
+ :time => ['DATE', 'TIMESTAMP', 'TIMESTAMP WITH TIME ZONE', 'TIMESTAMP WITH LOCAL TIME ZONE'],
111
+ :boolean => ['PL/SQL BOOLEAN'],
112
+ :hash => ['PL/SQL RECORD', 'OBJECT', 'PL/SQL TABLE'],
113
+ :array => ['TABLE', 'VARRAY'],
114
+ :cursor => ['REF CURSOR']
115
+ }
116
+ def matching_oracle_types_for_ruby_value(value)
117
+ case value
118
+ when NilClass
119
+ :all
120
+ when Fixnum, Bignum
121
+ MATCHING_TYPES[:integer]
122
+ when BigDecimal, Float
123
+ MATCHING_TYPES[:decimal]
124
+ when String
125
+ MATCHING_TYPES[:string]
126
+ when Date
127
+ MATCHING_TYPES[:date]
128
+ when Time
129
+ MATCHING_TYPES[:time]
130
+ when TrueClass, FalseClass
131
+ MATCHING_TYPES[:boolean]
132
+ when Hash
133
+ MATCHING_TYPES[:hash]
134
+ when Array
135
+ MATCHING_TYPES[:array]
136
+ when CursorCommon
137
+ MATCHING_TYPES[:cursor]
62
138
  end
63
139
  end
64
140
 
65
141
  def construct_sql(args)
66
- @declare_sql = "DECLARE\n"
67
- @assignment_sql = "BEGIN\n"
142
+ @declare_sql = ""
143
+ @assignment_sql = ""
68
144
  @call_sql = ""
69
145
  @return_sql = ""
70
146
  @return_vars = []
@@ -83,12 +159,13 @@ module PLSQL
83
159
  @bind_metadata = {}
84
160
 
85
161
  # Named arguments
86
- if args.size == 1 && args[0].is_a?(Hash) &&
87
- # do not use named arguments if procedure has just one PL/SQL record or object type argument -
162
+ # there should be just one Hash argument with symbol keys
163
+ if args.size == 1 && args[0].is_a?(Hash) && args[0].keys.all?{|k| k.is_a?(Symbol)} &&
164
+ # do not use named arguments if procedure has just one PL/SQL record PL/SQL table or object type argument -
88
165
  # in that case passed Hash should be used as value for this PL/SQL record argument
89
166
  # (which will be processed in sequential arguments bracnh)
90
167
  !(argument_list.size == 1 &&
91
- ['PL/SQL RECORD','OBJECT'].include?(arguments[(only_argument=argument_list[0])][:data_type]) &&
168
+ ['PL/SQL RECORD','PL/SQL TABLE','OBJECT'].include?(arguments[(only_argument=argument_list[0])][:data_type]) &&
92
169
  args[0].keys != [only_argument])
93
170
  # Add missing output arguments with nil value
94
171
  arguments.each do |arg, metadata|
@@ -96,6 +173,8 @@ module PLSQL
96
173
  args[0][arg] = nil
97
174
  end
98
175
  end
176
+ # Add SELF argument if provided
177
+ args[0][:self] = @self if @self
99
178
  # Add passed parameters to procedure call with parameter names
100
179
  @call_sql << args[0].map do |arg, value|
101
180
  "#{arg} => " << add_argument(arg, value)
@@ -103,6 +182,8 @@ module PLSQL
103
182
 
104
183
  # Sequential arguments
105
184
  else
185
+ # add SELF as first argument if provided
186
+ args.unshift(@self) if @self
106
187
  argument_count = argument_list.size
107
188
  raise ArgumentError, "Too many arguments passed to PL/SQL procedure" if args.size > argument_count
108
189
  # Add missing output arguments with nil value
@@ -125,62 +206,20 @@ module PLSQL
125
206
  else
126
207
  @call_sql = @procedure.call_sql(@call_sql)
127
208
  end
128
- add_out_vars
209
+ add_out_variables
129
210
 
130
211
  dbms_output_enable_sql, dbms_output_get_sql = dbms_output_sql
131
212
 
132
- @sql = "" << @declare_sql << @assignment_sql << dbms_output_enable_sql << @call_sql << dbms_output_get_sql << @return_sql << "END;\n"
213
+ @sql = @declare_sql.empty? ? "" : "DECLARE\n" << @declare_sql
214
+ @sql << "BEGIN\n" << @assignment_sql << dbms_output_enable_sql << @call_sql << dbms_output_get_sql << @return_sql << "END;\n"
133
215
  end
134
216
 
135
- def dbms_output_sql
136
- if @dbms_output_stream
137
- dbms_output_enable_sql = "DBMS_OUTPUT.ENABLE(#{@schema.dbms_output_buffer_size});\n"
138
- # if database version is at least 10.2 then use DBMS_OUTPUT.GET_LINES with SYS.DBMSOUTPUT_LINESARRAY
139
- if (@schema.connection.database_version <=> [10, 2]) >= 0
140
- @declare_sql << "l_dbms_output_numlines INTEGER := #{Schema::DBMS_OUTPUT_MAX_LINES};\n"
141
- dbms_output_get_sql = "DBMS_OUTPUT.GET_LINES(:dbms_output_lines, l_dbms_output_numlines);\n"
142
- @bind_values[:dbms_output_lines] = nil
143
- @bind_metadata[:dbms_output_lines] = {:data_type => 'TABLE', :data_length => nil,
144
- :sql_type_name => "SYS.DBMSOUTPUT_LINESARRAY", :in_out => 'OUT'}
145
- # if database version is less than 10.2 then use individual DBMS_OUTPUT.GET_LINE calls
146
- else
147
- dbms_output_get_sql = ""
148
- end
149
- [dbms_output_enable_sql, dbms_output_get_sql]
150
- else
151
- ["", ""]
152
- end
153
- end
154
-
155
- def dbms_output_log
156
- if @dbms_output_stream
157
- # if database version is at least 10.2 then :dbms_output_lines output bind variable has dbms_output lines
158
- if @bind_metadata[:dbms_output_lines]
159
- @cursor[':dbms_output_lines'].each do |line|
160
- @dbms_output_stream.puts "DBMS_OUTPUT: #{line}" if line
161
- end
162
- # if database version is less than 10.2 then use individual DBMS_OUTPUT.GET_LINE calls
163
- else
164
- cursor = @schema.connection.parse("BEGIN sys.dbms_output.get_line(:line, :status); END;")
165
- while true do
166
- cursor.bind_param(':line', nil, :data_type => 'VARCHAR2', :in_out => 'OUT')
167
- cursor.bind_param(':status', nil, :data_type => 'NUMBER', :in_out => 'OUT')
168
- cursor.exec
169
- break unless cursor[':status'] == 0
170
- @dbms_output_stream.puts "DBMS_OUTPUT: #{cursor[':line']}"
171
- end
172
- cursor.close
173
- end
174
- @dbms_output_stream.flush
175
- end
176
- end
177
-
178
- def add_argument(argument, value)
179
- argument_metadata = arguments[argument]
217
+ def add_argument(argument, value, argument_metadata=nil)
218
+ argument_metadata ||= arguments[argument]
180
219
  raise ArgumentError, "Wrong argument #{argument.inspect} passed to PL/SQL procedure" unless argument_metadata
181
220
  case argument_metadata[:data_type]
182
221
  when 'PL/SQL RECORD'
183
- @declare_sql << record_declaration_sql(argument, argument_metadata)
222
+ add_record_declaration(argument, argument_metadata)
184
223
  record_assignment_sql, record_bind_values, record_bind_metadata =
185
224
  record_assignment_sql_values_metadata(argument, argument_metadata, value)
186
225
  @assignment_sql << record_assignment_sql
@@ -194,22 +233,94 @@ module PLSQL
194
233
  @bind_metadata[argument] = argument_metadata.merge(:data_type => "NUMBER", :data_precision => 1)
195
234
  "l_#{argument}"
196
235
  else
197
- @bind_values[argument] = value
198
- @bind_metadata[argument] = argument_metadata
199
- ":#{argument}"
236
+ # TABLE or PL/SQL TABLE type defined inside package
237
+ if argument_metadata[:tmp_table_name]
238
+ add_table_declaration_and_assignment(argument, argument_metadata)
239
+ insert_values_into_tmp_table(argument, argument_metadata, value)
240
+ "l_#{argument}"
241
+ else
242
+ @bind_values[argument] = value
243
+ @bind_metadata[argument] = argument_metadata
244
+ ":#{argument}"
245
+ end
246
+ end
247
+ end
248
+
249
+ def add_table_declaration_and_assignment(argument, argument_metadata)
250
+ is_index_by_table = argument_metadata[:data_type] == 'PL/SQL TABLE'
251
+ @declare_sql << "l_#{argument} #{argument_metadata[:sql_type_name]}#{is_index_by_table ? nil : " := #{argument_metadata[:sql_type_name]}()"};\n"
252
+ @assignment_sql << "FOR r_#{argument} IN c_#{argument} LOOP\n"
253
+ @assignment_sql << "l_#{argument}.EXTEND;\n" unless is_index_by_table
254
+ case argument_metadata[:element][:data_type]
255
+ when 'PL/SQL RECORD'
256
+ fields = record_fields_sorted_by_position(argument_metadata[:element][:fields])
257
+ fields_string = is_index_by_table ? "*" : fields.join(', ')
258
+ @declare_sql << "CURSOR c_#{argument} IS SELECT #{fields_string} FROM #{argument_metadata[:tmp_table_name]} ORDER BY i__;\n"
259
+ if is_index_by_table
260
+ fields.each do |field|
261
+ @assignment_sql << "l_#{argument}(r_#{argument}.i__).#{field} := r_#{argument}.#{field};\n"
262
+ end
263
+ else
264
+ @assignment_sql << "l_#{argument}(l_#{argument}.COUNT) := r_#{argument};\n"
265
+ end
266
+ else
267
+ @declare_sql << "CURSOR c_#{argument} IS SELECT * FROM #{argument_metadata[:tmp_table_name]} ORDER BY i__;\n"
268
+ @assignment_sql << "l_#{argument}(r_#{argument}.i__) := r_#{argument}.element;\n"
269
+ end
270
+ @assignment_sql << "END LOOP;\n"
271
+ @assignment_sql << "DELETE FROM #{argument_metadata[:tmp_table_name]};\n"
272
+ end
273
+
274
+ def insert_values_into_tmp_table(argument, argument_metadata, values)
275
+ return unless values && !values.empty?
276
+ is_index_by_table = argument_metadata[:data_type] == 'PL/SQL TABLE'
277
+ if is_index_by_table
278
+ raise ArgumentError, "Hash value should be passed for #{argument.inspect} argument" unless values.is_a?(Hash)
279
+ else
280
+ raise ArgumentError, "Array value should be passed for #{argument.inspect} argument" unless values.is_a?(Array)
281
+ end
282
+ tmp_table = @schema.root_schema.send(argument_metadata[:tmp_table_name])
283
+ # insert values without autocommit
284
+ old_autocommit = @schema.connection.autocommit?
285
+ @schema.connection.autocommit = false if old_autocommit
286
+ case argument_metadata[:element][:data_type]
287
+ when 'PL/SQL RECORD'
288
+ values_with_index = []
289
+ if is_index_by_table
290
+ values.each{|i,v| values_with_index << v.merge(:i__ => i)}
291
+ else
292
+ values.each_with_index{|v,i| values_with_index << v.merge(:i__ => i+1)}
293
+ end
294
+ tmp_table.insert values_with_index
295
+ else
296
+ values_with_index = []
297
+ if is_index_by_table
298
+ values.each{|i,v| values_with_index << [v, i]}
299
+ else
300
+ values.each_with_index{|v,i| values_with_index << [v, i+1]}
301
+ end
302
+ tmp_table.insert_values [:element, :i__], *values_with_index
303
+ end
304
+ @schema.connection.autocommit = true if old_autocommit
305
+ end
306
+
307
+ def add_record_declaration(argument, argument_metadata)
308
+ @declare_sql << if argument_metadata[:type_subname]
309
+ "l_#{argument} #{argument_metadata[:sql_type_name]};\n"
310
+ else
311
+ fields_metadata = argument_metadata[:fields]
312
+ sql = "TYPE t_#{argument} IS RECORD (\n"
313
+ sql << record_fields_sorted_by_position(fields_metadata).map do |field|
314
+ metadata = fields_metadata[field]
315
+ "#{field} #{type_to_sql(metadata)}"
316
+ end.join(",\n")
317
+ sql << ");\n"
318
+ sql << "l_#{argument} t_#{argument};\n"
200
319
  end
201
320
  end
202
321
 
203
- def record_declaration_sql(argument, argument_metadata)
204
- fields_metadata = argument_metadata[:fields]
205
- sql = "TYPE t_#{argument} IS RECORD (\n"
206
- fields_sorted_by_position = fields_metadata.keys.sort_by{|k| fields_metadata[k][:position]}
207
- sql << fields_sorted_by_position.map do |field|
208
- metadata = fields_metadata[field]
209
- "#{field} #{type_to_sql(metadata)}"
210
- end.join(",\n")
211
- sql << ");\n"
212
- sql << "l_#{argument} t_#{argument};\n"
322
+ def record_fields_sorted_by_position(fields_metadata)
323
+ fields_metadata.keys.sort_by{|k| fields_metadata[k][:position]}
213
324
  end
214
325
 
215
326
  def record_assignment_sql_values_metadata(argument, argument_metadata, record_value)
@@ -229,136 +340,165 @@ module PLSQL
229
340
  end
230
341
 
231
342
  def add_return
232
- case return_metadata[:data_type]
343
+ add_return_variable(:return, return_metadata, true)
344
+ end
345
+
346
+ def add_out_variables
347
+ out_list.each do |argument|
348
+ add_return_variable(argument, arguments[argument])
349
+ end
350
+ end
351
+
352
+ def add_return_variable(argument, argument_metadata, is_return_value=false)
353
+ case argument_metadata[:data_type]
233
354
  when 'PL/SQL RECORD'
234
- @declare_sql << record_declaration_sql('return', return_metadata)
235
- return_metadata[:fields].each do |field, metadata|
236
- bind_variable = :"return_f#{metadata[:position]}"
355
+ add_record_declaration(argument, argument_metadata) if is_return_value
356
+ argument_metadata[:fields].each do |field, metadata|
357
+ # should use different output bind variable as JDBC does not support
358
+ # if output bind variable appears in several places
359
+ bind_variable = :"#{argument}_o#{metadata[:position]}"
237
360
  @return_vars << bind_variable
238
361
  @return_vars_metadata[bind_variable] = metadata
239
- @return_sql << ":#{bind_variable} := l_return.#{field};\n"
362
+ @return_sql << ":#{bind_variable} := l_#{argument}.#{field};\n"
240
363
  end
241
- "l_return := "
364
+ "l_#{argument} := " if is_return_value
242
365
  when 'PL/SQL BOOLEAN'
243
- @declare_sql << "l_return BOOLEAN;\n"
244
- @declare_sql << "x_return NUMBER(1);\n"
245
- @return_vars << :return
246
- @return_vars_metadata[:return] = return_metadata.merge(:data_type => "NUMBER", :data_precision => 1)
247
- @return_sql << "IF l_return IS NULL THEN\nx_return := NULL;\nELSIF l_return THEN\nx_return := 1;\nELSE\nx_return := 0;\nEND IF;\n" <<
248
- ":return := x_return;\n"
249
- "l_return := "
366
+ @declare_sql << "l_#{argument} BOOLEAN;\n" if is_return_value
367
+ @declare_sql << "o_#{argument} NUMBER(1);\n"
368
+ # should use different output bind variable as JDBC does not support
369
+ # if output bind variable appears in several places
370
+ bind_variable = :"o_#{argument}"
371
+ @return_vars << bind_variable
372
+ @return_vars_metadata[bind_variable] = argument_metadata.merge(:data_type => "NUMBER", :data_precision => 1)
373
+ @return_sql << "IF l_#{argument} IS NULL THEN\no_#{argument} := NULL;\n" <<
374
+ "ELSIF l_#{argument} THEN\no_#{argument} := 1;\nELSE\no_#{argument} := 0;\nEND IF;\n" <<
375
+ ":#{bind_variable} := o_#{argument};\n"
376
+ "l_#{argument} := " if is_return_value
250
377
  else
251
- @return_vars << :return
252
- @return_vars_metadata[:return] = return_metadata
253
- ':return := '
378
+ if argument_metadata[:tmp_table_name]
379
+ add_return_table(argument, argument_metadata, is_return_value)
380
+ elsif is_return_value
381
+ @return_vars << argument
382
+ @return_vars_metadata[argument] = argument_metadata
383
+ ":#{argument} := "
384
+ end
254
385
  end
255
386
  end
256
387
 
257
- def add_out_vars
258
- out_list.each do |argument|
259
- argument_metadata = arguments[argument]
260
- case argument_metadata[:data_type]
261
- when 'PL/SQL RECORD'
262
- argument_metadata[:fields].each do |field, metadata|
263
- bind_variable = :"#{argument}_o#{metadata[:position]}"
264
- @return_vars << bind_variable
265
- @return_vars_metadata[bind_variable] = metadata
266
- @return_sql << ":#{bind_variable} := l_#{argument}.#{field};\n"
267
- end
268
- when 'PL/SQL BOOLEAN'
269
- @declare_sql << "x_#{argument} NUMBER(1);\n"
270
- bind_variable = :"o_#{argument}"
271
- @return_vars << bind_variable
272
- @return_vars_metadata[bind_variable] = argument_metadata.merge(:data_type => "NUMBER", :data_precision => 1)
273
- @return_sql << "IF l_#{argument} IS NULL THEN\nx_#{argument} := NULL;\n" <<
274
- "ELSIF l_#{argument} THEN\nx_#{argument} := 1;\nELSE\nx_#{argument} := 0;\nEND IF;\n" <<
275
- ":#{bind_variable} := x_#{argument};\n"
276
- end
388
+ def add_return_table(argument, argument_metadata, is_return_value=false)
389
+ is_index_by_table = argument_metadata[:data_type] == 'PL/SQL TABLE'
390
+ declare_i__
391
+ @declare_sql << "l_return #{return_metadata[:sql_type_name]};\n" if is_return_value
392
+ @return_vars << argument
393
+ @return_vars_metadata[argument] = argument_metadata.merge(:data_type => "REF CURSOR")
394
+ @return_sql << if is_index_by_table
395
+ "i__ := l_#{argument}.FIRST;\nLOOP\nEXIT WHEN i__ IS NULL;\n"
396
+ else
397
+ "FOR i__ IN l_#{argument}.FIRST..l_#{argument}.LAST LOOP\n"
398
+ end
399
+ case argument_metadata[:element][:data_type]
400
+ when 'PL/SQL RECORD'
401
+ field_names = record_fields_sorted_by_position(argument_metadata[:element][:fields])
402
+ values_string = field_names.map{|f| "l_return(i__).#{f}"}.join(', ')
403
+ @return_sql << "INSERT INTO #{argument_metadata[:tmp_table_name]} VALUES (#{values_string}, i__);\n"
404
+ return_fields_string = is_index_by_table ? '*' : field_names.join(', ')
405
+ else
406
+ @return_sql << "INSERT INTO #{argument_metadata[:tmp_table_name]} VALUES (l_#{argument}(i__), i__);\n"
407
+ return_fields_string = '*'
277
408
  end
409
+ @return_sql << "i__ := l_#{argument}.NEXT(i__);\n" if is_index_by_table
410
+ @return_sql << "END LOOP;\n"
411
+ @return_sql << "OPEN :#{argument} FOR SELECT #{return_fields_string} FROM #{argument_metadata[:tmp_table_name]} ORDER BY i__;\n"
412
+ @return_sql << "DELETE FROM #{argument_metadata[:tmp_table_name]};\n"
413
+ "l_#{argument} := " if is_return_value
278
414
  end
279
415
 
280
- def type_to_sql(metadata)
281
- case metadata[:data_type]
282
- when 'NUMBER'
283
- precision, scale = metadata[:data_precision], metadata[:data_scale]
284
- "NUMBER#{precision ? "(#{precision}#{scale ? ",#{scale}": ""})" : ""}"
285
- when 'VARCHAR2', 'CHAR', 'NVARCHAR2', 'NCHAR'
286
- length = metadata[:data_length]
287
- if length && (char_used = metadata[:char_used])
288
- length = "#{length} #{char_used == 'C' ? 'CHAR' : 'BYTE'}"
289
- end
290
- "#{metadata[:data_type]}#{length ? "(#{length})": ""}"
291
- when 'TABLE', 'VARRAY', 'OBJECT'
292
- metadata[:sql_type_name]
293
- else
294
- metadata[:data_type]
416
+ # declare once temp variable i__ that is used as itertor
417
+ def declare_i__
418
+ unless @declared_i__
419
+ @declare_sql << "i__ PLS_INTEGER;\n"
420
+ @declared_i__ = true
295
421
  end
296
422
  end
297
423
 
424
+ def type_to_sql(metadata)
425
+ ProcedureCommon.type_to_sql(metadata)
426
+ end
427
+
298
428
  def get_return_value
299
429
  # if function with output parameters
300
430
  if return_metadata && out_list.size > 0
301
431
  result = [function_return_value, {}]
302
432
  out_list.each do |k|
303
- result[1][k] = out_var_value(k)
433
+ result[1][k] = out_variable_value(k)
304
434
  end
435
+ result
305
436
  # if function without output parameters
306
437
  elsif return_metadata
307
- result = function_return_value
438
+ function_return_value
308
439
  # if procedure with output parameters
309
440
  elsif out_list.size > 0
310
441
  result = {}
311
442
  out_list.each do |k|
312
- result[k] = out_var_value(k)
443
+ result[k] = out_variable_value(k)
313
444
  end
445
+ result
314
446
  # if procedure without output parameters
315
447
  else
316
- result = nil
448
+ nil
317
449
  end
318
- result
319
450
  end
320
451
 
321
452
  def function_return_value
322
- case return_metadata[:data_type]
323
- when 'PL/SQL RECORD'
324
- return_value = {}
325
- return_metadata[:fields].each do |field, metadata|
326
- bind_variable = :"return_f#{metadata[:position]}"
327
- return_value[field] = @cursor[":#{bind_variable}"]
328
- end
329
- return_value
330
- when 'PL/SQL BOOLEAN'
331
- numeric_value = @cursor[':return']
332
- numeric_value.nil? ? nil : numeric_value == 1
333
- else
334
- @cursor[':return']
335
- end
453
+ return_variable_value(:return, return_metadata)
454
+ end
455
+
456
+ def out_variable_value(argument)
457
+ return_variable_value(argument, arguments[argument])
336
458
  end
337
459
 
338
- def out_var_value(argument)
339
- argument_metadata = arguments[argument]
460
+ def return_variable_value(argument, argument_metadata)
340
461
  case argument_metadata[:data_type]
341
462
  when 'PL/SQL RECORD'
342
463
  return_value = {}
343
464
  argument_metadata[:fields].each do |field, metadata|
344
- bind_variable = :"#{argument}_o#{metadata[:position]}"
345
- return_value[field] = @cursor[":#{bind_variable}"]
465
+ return_value[field] = @cursor[":#{argument}_o#{metadata[:position]}"]
346
466
  end
347
467
  return_value
348
468
  when 'PL/SQL BOOLEAN'
349
469
  numeric_value = @cursor[":o_#{argument}"]
350
470
  numeric_value.nil? ? nil : numeric_value == 1
351
471
  else
352
- @cursor[":#{argument}"]
472
+ if argument_metadata[:tmp_table_name]
473
+ is_index_by_table = argument_metadata[:data_type] == 'PL/SQL TABLE'
474
+ case argument_metadata[:element][:data_type]
475
+ when 'PL/SQL RECORD'
476
+ if is_index_by_table
477
+ Hash[*@cursor[":#{argument}"].fetch_hash_all.map{|row| [row.delete(:i__), row]}.flatten]
478
+ else
479
+ @cursor[":#{argument}"].fetch_hash_all
480
+ end
481
+ else
482
+ if is_index_by_table
483
+ Hash[*@cursor[":#{argument}"].fetch_all.map{|row| [row[1], row[0]]}.flatten]
484
+ else
485
+ @cursor[":#{argument}"].fetch_all.map{|row| row[0]}
486
+ end
487
+ end
488
+ else
489
+ @cursor[":#{argument}"]
490
+ end
353
491
  end
354
492
  end
355
493
 
356
494
  def overload_argument_list
357
- @overload_argument_list ||= @procedure.argument_list
495
+ @overload_argument_list ||=
496
+ @skip_self ? @procedure.argument_list_without_self : @procedure.argument_list
358
497
  end
359
498
 
360
499
  def overload_arguments
361
- @overload_arguments ||= @procedure.arguments
500
+ @overload_arguments ||=
501
+ @skip_self ? @procedure.arguments_without_self : @procedure.arguments
362
502
  end
363
503
 
364
504
  def argument_list
@@ -374,7 +514,8 @@ module PLSQL
374
514
  end
375
515
 
376
516
  def out_list
377
- @out_list ||= @procedure.out_list[@overload]
517
+ @out_list ||=
518
+ @skip_self ? @procedure.out_list_without_self[@overload] : @procedure.out_list[@overload]
378
519
  end
379
520
 
380
521
  def schema_name
@@ -389,6 +530,49 @@ module PLSQL
389
530
  @procedure_name ||= @procedure.procedure
390
531
  end
391
532
 
533
+ def dbms_output_sql
534
+ if @dbms_output_stream
535
+ dbms_output_enable_sql = "DBMS_OUTPUT.ENABLE(#{@schema.dbms_output_buffer_size});\n"
536
+ # if database version is at least 10.2 then use DBMS_OUTPUT.GET_LINES with SYS.DBMSOUTPUT_LINESARRAY
537
+ if (@schema.connection.database_version <=> [10, 2]) >= 0
538
+ @declare_sql << "l_dbms_output_numlines INTEGER := #{Schema::DBMS_OUTPUT_MAX_LINES};\n"
539
+ dbms_output_get_sql = "DBMS_OUTPUT.GET_LINES(:dbms_output_lines, l_dbms_output_numlines);\n"
540
+ @bind_values[:dbms_output_lines] = nil
541
+ @bind_metadata[:dbms_output_lines] = {:data_type => 'TABLE', :data_length => nil,
542
+ :sql_type_name => "SYS.DBMSOUTPUT_LINESARRAY", :in_out => 'OUT'}
543
+ # if database version is less than 10.2 then use individual DBMS_OUTPUT.GET_LINE calls
544
+ else
545
+ dbms_output_get_sql = ""
546
+ end
547
+ [dbms_output_enable_sql, dbms_output_get_sql]
548
+ else
549
+ ["", ""]
550
+ end
551
+ end
552
+
553
+ def dbms_output_log
554
+ if @dbms_output_stream
555
+ # if database version is at least 10.2 then :dbms_output_lines output bind variable has dbms_output lines
556
+ if @bind_metadata[:dbms_output_lines]
557
+ @cursor[':dbms_output_lines'].each do |line|
558
+ @dbms_output_stream.puts "DBMS_OUTPUT: #{line}" if line
559
+ end
560
+ # if database version is less than 10.2 then use individual DBMS_OUTPUT.GET_LINE calls
561
+ else
562
+ cursor = @schema.connection.parse("BEGIN sys.dbms_output.get_line(:line, :status); END;")
563
+ while true do
564
+ cursor.bind_param(':line', nil, :data_type => 'VARCHAR2', :in_out => 'OUT')
565
+ cursor.bind_param(':status', nil, :data_type => 'NUMBER', :in_out => 'OUT')
566
+ cursor.exec
567
+ break unless cursor[':status'] == 0
568
+ @dbms_output_stream.puts "DBMS_OUTPUT: #{cursor[':line']}"
569
+ end
570
+ cursor.close
571
+ end
572
+ @dbms_output_stream.flush
573
+ end
574
+ end
575
+
392
576
  end
393
577
 
394
578
  end