flash-gordons-ruby-plsql 0.5.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.
- checksums.yaml +15 -0
- data/Gemfile +14 -0
- data/History.txt +172 -0
- data/License.txt +20 -0
- data/README.md +182 -0
- data/Rakefile +47 -0
- data/VERSION +1 -0
- data/lib/plsql/connection.rb +233 -0
- data/lib/plsql/helpers.rb +9 -0
- data/lib/plsql/jdbc_connection.rb +542 -0
- data/lib/plsql/oci8_patches.rb +25 -0
- data/lib/plsql/oci_connection.rb +339 -0
- data/lib/plsql/package.rb +80 -0
- data/lib/plsql/procedure.rb +269 -0
- data/lib/plsql/procedure_call.rb +124 -0
- data/lib/plsql/schema.rb +309 -0
- data/lib/plsql/sequence.rb +49 -0
- data/lib/plsql/sql_statements.rb +87 -0
- data/lib/plsql/table.rb +348 -0
- data/lib/plsql/type.rb +275 -0
- data/lib/plsql/variable.rb +146 -0
- data/lib/plsql/version.rb +3 -0
- data/lib/plsql/view.rb +41 -0
- data/lib/ruby-plsql.rb +1 -0
- data/lib/ruby_plsql.rb +18 -0
- data/ruby-plsql.gemspec +87 -0
- data/spec/plsql/connection_spec.rb +495 -0
- data/spec/plsql/package_spec.rb +149 -0
- data/spec/plsql/procedure_spec.rb +2048 -0
- data/spec/plsql/schema_spec.rb +331 -0
- data/spec/plsql/sequence_spec.rb +67 -0
- data/spec/plsql/sql_statements_spec.rb +91 -0
- data/spec/plsql/table_spec.rb +371 -0
- data/spec/plsql/type_spec.rb +304 -0
- data/spec/plsql/variable_spec.rb +505 -0
- data/spec/plsql/version_spec.rb +8 -0
- data/spec/plsql/view_spec.rb +264 -0
- data/spec/spec.opts +6 -0
- data/spec/spec_helper.rb +79 -0
- metadata +159 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
# apply TIMESTAMP fractional seconds patch to ruby-oci8 2.0.3
|
2
|
+
# see http://rubyforge.org/forum/forum.php?thread_id=46576&forum_id=1078
|
3
|
+
if OCI8::VERSION == "2.0.3" &&
|
4
|
+
!OCI8::BindType::Util.method_defined?(:datetime_to_array_without_timestamp_patch)
|
5
|
+
|
6
|
+
OCI8::BindType::Util.module_eval do
|
7
|
+
alias :datetime_to_array_without_timestamp_patch :datetime_to_array
|
8
|
+
def datetime_to_array(val, full)
|
9
|
+
result = datetime_to_array_without_timestamp_patch(val, full)
|
10
|
+
if result && result[6] == 0
|
11
|
+
if val.respond_to? :nsec
|
12
|
+
fsec = val.nsec
|
13
|
+
elsif val.respond_to? :usec
|
14
|
+
fsec = val.usec * 1000
|
15
|
+
else
|
16
|
+
fsec = 0
|
17
|
+
end
|
18
|
+
result[6] = fsec
|
19
|
+
end
|
20
|
+
result
|
21
|
+
end
|
22
|
+
private :datetime_to_array_without_timestamp_patch, :datetime_to_array
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,339 @@
|
|
1
|
+
begin
|
2
|
+
require "oci8"
|
3
|
+
rescue LoadError
|
4
|
+
# OCI8 driver is unavailable.
|
5
|
+
msg = $!.to_s
|
6
|
+
if /-- oci8$/ =~ msg
|
7
|
+
# ruby-oci8 is not installed.
|
8
|
+
# MRI <= 1.9.2, Rubinius, JRuby:
|
9
|
+
# no such file to load -- oci8
|
10
|
+
# MRI >= 1.9.3:
|
11
|
+
# cannot load such file -- oci8
|
12
|
+
msg = "Please install ruby-oci8 gem."
|
13
|
+
end
|
14
|
+
raise LoadError, "ERROR: ruby-plsql could not load ruby-oci8 library. #{msg}"
|
15
|
+
end
|
16
|
+
|
17
|
+
require "plsql/oci8_patches"
|
18
|
+
|
19
|
+
# check ruby-oci8 version
|
20
|
+
required_oci8_version = [2, 0, 3]
|
21
|
+
oci8_version_ints = OCI8::VERSION.scan(/\d+/).map{|s| s.to_i}
|
22
|
+
if (oci8_version_ints <=> required_oci8_version) < 0
|
23
|
+
raise LoadError, "ERROR: ruby-oci8 version #{OCI8::VERSION} is too old. Please install ruby-oci8 version #{required_oci8_version.join('.')} or later."
|
24
|
+
end
|
25
|
+
|
26
|
+
module PLSQL
|
27
|
+
class OCIConnection < Connection #:nodoc:
|
28
|
+
|
29
|
+
def self.create_raw(params)
|
30
|
+
connection_string = if params[:host]
|
31
|
+
"//#{params[:host]}:#{params[:port]||1521}/#{params[:database]}"
|
32
|
+
else
|
33
|
+
params[:database]
|
34
|
+
end
|
35
|
+
new(OCI8.new(params[:username], params[:password], connection_string))
|
36
|
+
end
|
37
|
+
|
38
|
+
def logoff
|
39
|
+
super
|
40
|
+
raw_connection.logoff
|
41
|
+
end
|
42
|
+
|
43
|
+
def commit
|
44
|
+
raw_connection.commit
|
45
|
+
end
|
46
|
+
|
47
|
+
def rollback
|
48
|
+
raw_connection.rollback
|
49
|
+
end
|
50
|
+
|
51
|
+
def autocommit?
|
52
|
+
raw_connection.autocommit?
|
53
|
+
end
|
54
|
+
|
55
|
+
def autocommit=(value)
|
56
|
+
raw_connection.autocommit = value
|
57
|
+
end
|
58
|
+
|
59
|
+
def prefetch_rows=(value)
|
60
|
+
raw_connection.prefetch_rows = value
|
61
|
+
end
|
62
|
+
|
63
|
+
def exec(sql, *bindvars)
|
64
|
+
raw_connection.exec(sql, *bindvars)
|
65
|
+
true
|
66
|
+
end
|
67
|
+
|
68
|
+
class Cursor #:nodoc:
|
69
|
+
include Connection::CursorCommon
|
70
|
+
|
71
|
+
# stack of open cursors
|
72
|
+
@@open_cursors = []
|
73
|
+
attr_reader :raw_cursor
|
74
|
+
|
75
|
+
def initialize(conn, raw_cursor)
|
76
|
+
@connection = conn
|
77
|
+
@raw_cursor = raw_cursor
|
78
|
+
@@open_cursors.push self
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.new_from_parse(conn, sql)
|
82
|
+
raw_cursor = conn.raw_connection.parse(sql)
|
83
|
+
self.new(conn, raw_cursor)
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.new_from_query(conn, sql, bindvars=[], options={})
|
87
|
+
cursor = new_from_parse(conn, sql)
|
88
|
+
if prefetch_rows = options[:prefetch_rows]
|
89
|
+
cursor.prefetch_rows = prefetch_rows
|
90
|
+
end
|
91
|
+
cursor.exec(*bindvars)
|
92
|
+
cursor
|
93
|
+
end
|
94
|
+
|
95
|
+
def prefetch_rows=(value)
|
96
|
+
@raw_cursor.prefetch_rows = value
|
97
|
+
end
|
98
|
+
|
99
|
+
def bind_param(arg, value, metadata)
|
100
|
+
type, length = @connection.plsql_to_ruby_data_type(metadata)
|
101
|
+
ora_value = @connection.ruby_value_to_ora_value(value, type)
|
102
|
+
@raw_cursor.bind_param(arg, ora_value, type, length)
|
103
|
+
end
|
104
|
+
|
105
|
+
def exec(*bindvars)
|
106
|
+
@raw_cursor.exec(*bindvars)
|
107
|
+
end
|
108
|
+
|
109
|
+
def [](key)
|
110
|
+
@connection.ora_value_to_ruby_value(@raw_cursor[key])
|
111
|
+
end
|
112
|
+
|
113
|
+
def fetch
|
114
|
+
row = @raw_cursor.fetch
|
115
|
+
row && row.map{|v| @connection.ora_value_to_ruby_value(v)}
|
116
|
+
end
|
117
|
+
|
118
|
+
def fields
|
119
|
+
@fields ||= @raw_cursor.get_col_names.map{|c| c.downcase.to_sym}
|
120
|
+
end
|
121
|
+
|
122
|
+
def close_raw_cursor
|
123
|
+
@raw_cursor.close
|
124
|
+
end
|
125
|
+
|
126
|
+
def close
|
127
|
+
# close all cursors that were created after this one
|
128
|
+
while (open_cursor = @@open_cursors.pop) && !open_cursor.equal?(self)
|
129
|
+
open_cursor.close_raw_cursor
|
130
|
+
end
|
131
|
+
close_raw_cursor
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
def parse(sql)
|
137
|
+
Cursor.new_from_parse(self, sql)
|
138
|
+
end
|
139
|
+
|
140
|
+
def cursor_from_query(sql, bindvars=[], options={})
|
141
|
+
Cursor.new_from_query(self, sql, bindvars, options)
|
142
|
+
end
|
143
|
+
|
144
|
+
def plsql_to_ruby_data_type(metadata)
|
145
|
+
data_type, data_length = metadata[:data_type], metadata[:data_length]
|
146
|
+
case data_type
|
147
|
+
when "VARCHAR2", "CHAR", "NVARCHAR2", "NCHAR"
|
148
|
+
[String, data_length || 32767]
|
149
|
+
when "CLOB", "NCLOB"
|
150
|
+
[OCI8::CLOB, nil]
|
151
|
+
when "BLOB"
|
152
|
+
[OCI8::BLOB, nil]
|
153
|
+
when "NUMBER", "PLS_INTEGER", "BINARY_INTEGER"
|
154
|
+
[OraNumber, nil]
|
155
|
+
when "DATE"
|
156
|
+
[DateTime, nil]
|
157
|
+
when "TIMESTAMP", "TIMESTAMP WITH TIME ZONE", "TIMESTAMP WITH LOCAL TIME ZONE"
|
158
|
+
[Time, nil]
|
159
|
+
when "TABLE", "VARRAY", "OBJECT"
|
160
|
+
# create Ruby class for collection
|
161
|
+
klass = OCI8::Object::Base.get_class_by_typename(metadata[:sql_type_name])
|
162
|
+
unless klass
|
163
|
+
klass = Class.new(OCI8::Object::Base)
|
164
|
+
klass.set_typename metadata[:sql_type_name]
|
165
|
+
end
|
166
|
+
[klass, nil]
|
167
|
+
when "REF CURSOR"
|
168
|
+
[OCI8::Cursor]
|
169
|
+
else
|
170
|
+
[String, 32767]
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def ruby_value_to_ora_value(value, type=nil)
|
175
|
+
type ||= value.class
|
176
|
+
case type.to_s.to_sym
|
177
|
+
when :Fixnum, :BigDecimal, :String
|
178
|
+
value
|
179
|
+
when :OraNumber
|
180
|
+
# pass parameters as OraNumber to avoid rounding errors
|
181
|
+
case value
|
182
|
+
when Bignum
|
183
|
+
OraNumber.new(value.to_s)
|
184
|
+
when BigDecimal
|
185
|
+
OraNumber.new(value.to_s('F'))
|
186
|
+
when TrueClass
|
187
|
+
OraNumber.new(1)
|
188
|
+
when FalseClass
|
189
|
+
OraNumber.new(0)
|
190
|
+
else
|
191
|
+
value
|
192
|
+
end
|
193
|
+
when :DateTime
|
194
|
+
case value
|
195
|
+
when Time
|
196
|
+
::DateTime.civil(value.year, value.month, value.day, value.hour, value.min, value.sec, Rational(value.utc_offset, 86400))
|
197
|
+
when DateTime
|
198
|
+
value
|
199
|
+
when Date
|
200
|
+
::DateTime.civil(value.year, value.month, value.day, 0, 0, 0, 0)
|
201
|
+
else
|
202
|
+
value
|
203
|
+
end
|
204
|
+
when :"OCI8::CLOB", :"OCI8::BLOB"
|
205
|
+
# ruby-oci8 cannot create CLOB/BLOB from ''
|
206
|
+
value.to_s.length > 0 ? type.new(raw_oci_connection, value) : nil
|
207
|
+
when :"OCI8::Cursor"
|
208
|
+
value && value.raw_cursor
|
209
|
+
else
|
210
|
+
# collections and object types
|
211
|
+
if type.superclass == OCI8::Object::Base
|
212
|
+
return nil if value.nil?
|
213
|
+
tdo = raw_oci_connection.get_tdo_by_class(type)
|
214
|
+
if tdo.is_collection?
|
215
|
+
raise ArgumentError, "You should pass Array value for collection type parameter" unless value.is_a?(Array)
|
216
|
+
elem_list = value.map do |elem|
|
217
|
+
if (attr_tdo = tdo.coll_attr.typeinfo)
|
218
|
+
attr_type, attr_length = plsql_to_ruby_data_type(:data_type => 'OBJECT', :sql_type_name => attr_tdo.typename)
|
219
|
+
else
|
220
|
+
attr_type = elem.class
|
221
|
+
end
|
222
|
+
ruby_value_to_ora_value(elem, attr_type)
|
223
|
+
end
|
224
|
+
# construct collection value
|
225
|
+
# TODO: change setting instance variable to appropriate ruby-oci8 method call when available
|
226
|
+
collection = type.new(raw_oci_connection)
|
227
|
+
collection.instance_variable_set('@attributes', elem_list)
|
228
|
+
collection
|
229
|
+
else # object type
|
230
|
+
raise ArgumentError, "You should pass Hash value for object type parameter" unless value.is_a?(Hash)
|
231
|
+
object_attrs = value.dup
|
232
|
+
object_attrs.keys.each do |key|
|
233
|
+
raise ArgumentError, "Wrong object type field passed to PL/SQL procedure" unless (attr = tdo.attr_getters[key])
|
234
|
+
case attr.datatype
|
235
|
+
when OCI8::TDO::ATTR_NAMED_TYPE, OCI8::TDO::ATTR_NAMED_COLLECTION
|
236
|
+
# nested object type or collection
|
237
|
+
attr_type, attr_length = plsql_to_ruby_data_type(:data_type => 'OBJECT', :sql_type_name => attr.typeinfo.typename)
|
238
|
+
object_attrs[key] = ruby_value_to_ora_value(object_attrs[key], attr_type)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
type.new(raw_oci_connection, object_attrs)
|
242
|
+
end
|
243
|
+
# all other cases
|
244
|
+
else
|
245
|
+
value
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def ora_value_to_ruby_value(value)
|
251
|
+
case value
|
252
|
+
when Float, OraNumber, BigDecimal
|
253
|
+
ora_number_to_ruby_number(value)
|
254
|
+
when DateTime, OraDate
|
255
|
+
ora_date_to_ruby_date(value)
|
256
|
+
when OCI8::LOB
|
257
|
+
if value.available?
|
258
|
+
value.rewind
|
259
|
+
value.read
|
260
|
+
else
|
261
|
+
nil
|
262
|
+
end
|
263
|
+
when OCI8::Object::Base
|
264
|
+
tdo = raw_oci_connection.get_tdo_by_class(value.class)
|
265
|
+
if tdo.is_collection?
|
266
|
+
value.to_ary.map{|e| ora_value_to_ruby_value(e)}
|
267
|
+
else # object type
|
268
|
+
tdo.attributes.inject({}) do |hash, attr|
|
269
|
+
hash[attr.name] = ora_value_to_ruby_value(value.instance_variable_get(:@attributes)[attr.name])
|
270
|
+
hash
|
271
|
+
end
|
272
|
+
end
|
273
|
+
when OCI8::Cursor
|
274
|
+
Cursor.new(self, value)
|
275
|
+
else
|
276
|
+
value
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
def describe_synonym(schema_name, synonym_name)
|
281
|
+
if schema_name == 'PUBLIC'
|
282
|
+
full_name = synonym_name.to_s
|
283
|
+
else
|
284
|
+
full_name = "#{schema_name}.#{synonym_name}"
|
285
|
+
end
|
286
|
+
metadata = raw_connection.describe_synonym(full_name)
|
287
|
+
[metadata.schema_name, metadata.name]
|
288
|
+
rescue OCIError
|
289
|
+
nil
|
290
|
+
end
|
291
|
+
|
292
|
+
def database_version
|
293
|
+
@database_version ||= (version = raw_connection.oracle_server_version) &&
|
294
|
+
[version.major, version.minor, version.update, version.patch]
|
295
|
+
end
|
296
|
+
|
297
|
+
private
|
298
|
+
|
299
|
+
def raw_oci_connection
|
300
|
+
if raw_connection.is_a? OCI8
|
301
|
+
raw_connection
|
302
|
+
# ActiveRecord Oracle enhanced adapter puts OCI8EnhancedAutoRecover wrapper around OCI8
|
303
|
+
# in this case we need to pass original OCI8 connection
|
304
|
+
else
|
305
|
+
raw_connection.instance_variable_get(:@connection)
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
def ora_number_to_ruby_number(num)
|
310
|
+
# return BigDecimal instead of Float to avoid rounding errors
|
311
|
+
num == (num_to_i = num.to_i) ? num_to_i : (num.is_a?(BigDecimal) ? num : BigDecimal.new(num.to_s))
|
312
|
+
end
|
313
|
+
|
314
|
+
def ora_date_to_ruby_date(val)
|
315
|
+
case val
|
316
|
+
when DateTime
|
317
|
+
# similar implementation as in oracle_enhanced adapter
|
318
|
+
begin
|
319
|
+
Time.send(plsql.default_timezone, val.year, val.month, val.day, val.hour, val.min, val.sec)
|
320
|
+
rescue
|
321
|
+
offset = plsql.default_timezone.to_sym == :local ? plsql.local_timezone_offset : 0
|
322
|
+
DateTime.civil(val.year, val.month, val.day, val.hour, val.min, val.sec, offset)
|
323
|
+
end
|
324
|
+
when OraDate
|
325
|
+
# similar implementation as in oracle_enhanced adapter
|
326
|
+
begin
|
327
|
+
Time.send(plsql.default_timezone, val.year, val.month, val.day, val.hour, val.minute, val.second)
|
328
|
+
rescue
|
329
|
+
offset = plsql.default_timezone.to_sym == :local ? plsql.local_timezone_offset : 0
|
330
|
+
DateTime.civil(val.year, val.month, val.day, val.hour, val.minute, val.second, offset)
|
331
|
+
end
|
332
|
+
else
|
333
|
+
val
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
end
|
338
|
+
|
339
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module PLSQL
|
2
|
+
module PackageClassMethods #:nodoc:
|
3
|
+
def find(schema, package_name)
|
4
|
+
find_package_in_schema(schema, package_name) || find_package_by_synonym(schema, package_name)
|
5
|
+
end
|
6
|
+
|
7
|
+
def find_by_db_object(db_object)
|
8
|
+
find(db_object.schema, db_object.name)
|
9
|
+
end
|
10
|
+
|
11
|
+
def find_package_in_schema(schema, package_name)
|
12
|
+
row = schema.select_first(<<-SQL, schema.schema_name, package_name.to_s.upcase)
|
13
|
+
SELECT object_name
|
14
|
+
FROM all_objects
|
15
|
+
WHERE owner = :owner
|
16
|
+
AND object_name = :package
|
17
|
+
AND object_type = 'PACKAGE'
|
18
|
+
SQL
|
19
|
+
new(schema, package_name) if row
|
20
|
+
end
|
21
|
+
|
22
|
+
def find_package_by_synonym(schema, package_name)
|
23
|
+
row = schema.select_first(<<-SQL, schema.schema_name, package_name.to_s.upcase)
|
24
|
+
SELECT o.owner, o.object_name
|
25
|
+
FROM all_synonyms s,
|
26
|
+
all_objects o
|
27
|
+
WHERE s.owner IN (:owner, 'PUBLIC')
|
28
|
+
AND s.synonym_name = :synonym_name
|
29
|
+
AND o.owner = s.table_owner
|
30
|
+
AND o.object_name = s.table_name
|
31
|
+
AND o.object_type = 'PACKAGE'
|
32
|
+
ORDER BY DECODE(s.owner, 'PUBLIC', 1, 0)
|
33
|
+
SQL
|
34
|
+
new(schema, row[1], row[0]) if row
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class Package #:nodoc:
|
39
|
+
extend PackageClassMethods
|
40
|
+
|
41
|
+
def initialize(schema, package, override_schema_name = nil)
|
42
|
+
@schema = schema
|
43
|
+
@override_schema_name = override_schema_name
|
44
|
+
@package = package.to_s.upcase
|
45
|
+
@package_objects = {}
|
46
|
+
end
|
47
|
+
|
48
|
+
def [](object_name)
|
49
|
+
object_name = object_name.to_s
|
50
|
+
@package_objects[object_name] ||= [Procedure, Variable, PipelinedFunction].inject(nil) do |res, object_type|
|
51
|
+
res || object_type.find(@schema, object_name, @package, @override_schema_name)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def method_missing(method, *args, &block)
|
58
|
+
method = method.to_s
|
59
|
+
method.chop! if (assignment = method[/=$/])
|
60
|
+
|
61
|
+
case (object = self[method])
|
62
|
+
when Procedure, PipelinedFunction
|
63
|
+
raise ArgumentError, "Cannot assign value to package procedure '#{method.upcase}'" if assignment
|
64
|
+
object.exec(*args, &block)
|
65
|
+
when Variable
|
66
|
+
if assignment
|
67
|
+
raise ArgumentError, "Just one value can be assigned to package variable '#{method.upcase}'" unless args.size == 1 && block.nil?
|
68
|
+
object.value = args[0]
|
69
|
+
else
|
70
|
+
raise ArgumentError, "Cannot pass arguments when getting package variable '#{method.upcase}' value" unless args.size == 0 && block.nil?
|
71
|
+
object.value
|
72
|
+
end
|
73
|
+
else
|
74
|
+
raise ArgumentError, "No PL/SQL procedure or variable '#{method.upcase}' found"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
@@ -0,0 +1,269 @@
|
|
1
|
+
module PLSQL
|
2
|
+
|
3
|
+
module ProcedureClassMethods #:nodoc:
|
4
|
+
def find(schema, procedure_name, package_name = nil, override_schema_name = nil)
|
5
|
+
if package_name
|
6
|
+
find_procedure_in_package(schema, package_name, procedure_name, override_schema_name)
|
7
|
+
else
|
8
|
+
find_procedure_in_schema(schema, procedure_name) || find_procedure_by_synonym(schema, procedure_name)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def find_procedure_in_schema(schema, procedure_name)
|
13
|
+
row = schema.select_first(<<-SQL, schema.schema_name, procedure_name.to_s.upcase)
|
14
|
+
SELECT object_id
|
15
|
+
FROM all_procedures
|
16
|
+
WHERE owner = :owner
|
17
|
+
AND object_name = :object_name
|
18
|
+
AND object_type IN ('PROCEDURE', 'FUNCTION')
|
19
|
+
AND pipelined = 'NO'
|
20
|
+
SQL
|
21
|
+
new(schema, procedure_name, nil, nil, row[0]) if row
|
22
|
+
end
|
23
|
+
|
24
|
+
def find_procedure_by_synonym(schema, procedure_name)
|
25
|
+
row = schema.select_first(<<-SQL, schema.schema_name, procedure_name.to_s.upcase)
|
26
|
+
SELECT p.owner, p.object_name, p.object_id
|
27
|
+
FROM all_synonyms s,
|
28
|
+
all_procedures p
|
29
|
+
WHERE s.owner IN (:owner, 'PUBLIC')
|
30
|
+
AND s.synonym_name = :synonym_name
|
31
|
+
AND p.owner = s.table_owner
|
32
|
+
AND p.object_name = s.table_name
|
33
|
+
AND p.object_type IN ('PROCEDURE','FUNCTION')
|
34
|
+
AND p.pipelined = 'NO'
|
35
|
+
ORDER BY DECODE(s.owner, 'PUBLIC', 1, 0)
|
36
|
+
SQL
|
37
|
+
new(schema, row[1], nil, row[0], row[2]) if row
|
38
|
+
end
|
39
|
+
|
40
|
+
def find_procedure_in_package(schema, package_name, procedure_name, override_schema_name = nil)
|
41
|
+
schema_name = override_schema_name || schema.schema_name
|
42
|
+
row = schema.select_first(<<-SQL, schema_name, package_name, procedure_name.to_s.upcase)
|
43
|
+
SELECT o.object_id
|
44
|
+
FROM all_procedures p,
|
45
|
+
all_objects o
|
46
|
+
WHERE p.owner = :owner
|
47
|
+
AND p.object_name = :object_name
|
48
|
+
AND p.procedure_name = :procedure_name
|
49
|
+
AND p.pipelined = 'NO'
|
50
|
+
AND o.owner = p.owner
|
51
|
+
AND o.object_name = p.object_name
|
52
|
+
AND o.object_type = 'PACKAGE'
|
53
|
+
SQL
|
54
|
+
new(schema, procedure_name, package_name, override_schema_name, row[0]) if row
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
module ProcedureCommon #:nodoc:
|
59
|
+
attr_reader :arguments, :argument_list, :out_list, :return
|
60
|
+
attr_reader :schema, :schema_name, :package, :procedure
|
61
|
+
|
62
|
+
# return type string from metadata that can be used in DECLARE block or table definition
|
63
|
+
def self.type_to_sql(metadata) #:nodoc:
|
64
|
+
case metadata[:data_type]
|
65
|
+
when 'NUMBER'
|
66
|
+
precision, scale = metadata[:data_precision], metadata[:data_scale]
|
67
|
+
"NUMBER#{precision ? "(#{precision}#{scale ? ",#{scale}": ""})" : ""}"
|
68
|
+
when 'VARCHAR2', 'CHAR'
|
69
|
+
length = case metadata[:char_used]
|
70
|
+
when 'C' then "#{metadata[:char_length]} CHAR"
|
71
|
+
when 'B' then "#{metadata[:data_length]} BYTE"
|
72
|
+
else
|
73
|
+
metadata[:data_length]
|
74
|
+
end
|
75
|
+
"#{metadata[:data_type]}#{length && "(#{length})"}"
|
76
|
+
when 'NVARCHAR2', 'NCHAR'
|
77
|
+
length = metadata[:char_length]
|
78
|
+
"#{metadata[:data_type]}#{length && "(#{length})"}"
|
79
|
+
when 'PL/SQL TABLE', 'TABLE', 'VARRAY', 'OBJECT'
|
80
|
+
metadata[:sql_type_name]
|
81
|
+
else
|
82
|
+
metadata[:data_type]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# get procedure argument metadata from data dictionary
|
87
|
+
def get_argument_metadata #:nodoc:
|
88
|
+
@arguments = {}
|
89
|
+
@argument_list = {}
|
90
|
+
@out_list = {}
|
91
|
+
@return = {}
|
92
|
+
@overloaded = false
|
93
|
+
|
94
|
+
# store reference to previous level record or collection metadata
|
95
|
+
previous_level_argument_metadata = {}
|
96
|
+
|
97
|
+
# store tmp tables for each overload for table parameters with types defined inside packages
|
98
|
+
@tmp_table_names = {}
|
99
|
+
# store if tmp tables are created for specific overload
|
100
|
+
@tmp_tables_created = {}
|
101
|
+
|
102
|
+
# subprogram_id column is available just from version 10g
|
103
|
+
subprogram_id_column = (@schema.connection.database_version <=> [10, 2, 0, 2]) >= 0 ? 'subprogram_id' : 'NULL'
|
104
|
+
|
105
|
+
@schema.select_all(
|
106
|
+
"SELECT #{subprogram_id_column}, object_name, TO_NUMBER(overload), argument_name, position, data_level,
|
107
|
+
data_type, in_out, data_length, data_precision, data_scale, char_used,
|
108
|
+
char_length, type_owner, type_name, type_subname
|
109
|
+
FROM all_arguments
|
110
|
+
WHERE object_id = :object_id
|
111
|
+
AND owner = :owner
|
112
|
+
AND object_name = :procedure_name
|
113
|
+
ORDER BY overload, sequence",
|
114
|
+
@object_id, @schema_name, @procedure
|
115
|
+
) do |r|
|
116
|
+
|
117
|
+
subprogram_id, object_name, overload, argument_name, position, data_level,
|
118
|
+
data_type, in_out, data_length, data_precision, data_scale, char_used,
|
119
|
+
char_length, type_owner, type_name, type_subname = r
|
120
|
+
|
121
|
+
@overloaded ||= !overload.nil?
|
122
|
+
# if not overloaded then store arguments at key 0
|
123
|
+
overload ||= 0
|
124
|
+
@arguments[overload] ||= {}
|
125
|
+
@return[overload] ||= nil
|
126
|
+
@tmp_table_names[overload] ||= []
|
127
|
+
|
128
|
+
sql_type_name = type_owner && "#{type_owner == 'PUBLIC' ? nil : "#{type_owner}."}#{type_name}#{type_subname ? ".#{type_subname}" : nil}"
|
129
|
+
|
130
|
+
tmp_table_name = nil
|
131
|
+
# type defined inside package
|
132
|
+
if type_subname
|
133
|
+
if collection_type?(data_type)
|
134
|
+
raise ArgumentError, "#{data_type} type #{sql_type_name} definition inside package is not supported as part of other type definition," <<
|
135
|
+
" use CREATE TYPE outside package" if data_level > 0
|
136
|
+
# if subprogram_id was not supported by all_arguments view
|
137
|
+
# then generate unique ID from object_name and overload
|
138
|
+
subprogram_id ||= "#{object_name.hash % 10000}#{overload}"
|
139
|
+
tmp_table_name = "#{Connection::RUBY_TEMP_TABLE_PREFIX}#{@schema.connection.session_id}_#{@object_id}_#{subprogram_id}_#{position}"
|
140
|
+
elsif data_type != 'PL/SQL RECORD'
|
141
|
+
# raise exception only when there are no overloaded procedure definitions
|
142
|
+
# (as probably this overload will not be used at all)
|
143
|
+
raise ArgumentError, "Parameter type #{sql_type_name} definition inside package is not supported, use CREATE TYPE outside package" if overload == 0
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
argument_metadata = {
|
148
|
+
:position => position && position.to_i,
|
149
|
+
:data_type => data_type,
|
150
|
+
:in_out => in_out,
|
151
|
+
:data_length => data_length && data_length.to_i,
|
152
|
+
:data_precision => data_precision && data_precision.to_i,
|
153
|
+
:data_scale => data_scale && data_scale.to_i,
|
154
|
+
:char_used => char_used,
|
155
|
+
:char_length => char_length && char_length.to_i,
|
156
|
+
:type_owner => type_owner,
|
157
|
+
:type_name => type_name,
|
158
|
+
:type_subname => type_subname,
|
159
|
+
:sql_type_name => sql_type_name
|
160
|
+
}
|
161
|
+
if tmp_table_name
|
162
|
+
@tmp_table_names[overload] << [(argument_metadata[:tmp_table_name] = tmp_table_name), argument_metadata]
|
163
|
+
end
|
164
|
+
|
165
|
+
if composite_type?(data_type)
|
166
|
+
case data_type
|
167
|
+
when 'PL/SQL RECORD'
|
168
|
+
argument_metadata[:fields] = {}
|
169
|
+
end
|
170
|
+
previous_level_argument_metadata[data_level] = argument_metadata
|
171
|
+
end
|
172
|
+
|
173
|
+
# if function has return value
|
174
|
+
if argument_name.nil? && data_level == 0 && in_out == 'OUT'
|
175
|
+
@return[overload] = argument_metadata
|
176
|
+
# if parameter
|
177
|
+
else
|
178
|
+
# top level parameter
|
179
|
+
if data_level == 0
|
180
|
+
# sometime there are empty IN arguments in all_arguments view for procedures without arguments (e.g. for DBMS_OUTPUT.DISABLE)
|
181
|
+
@arguments[overload][argument_name.downcase.to_sym] = argument_metadata if argument_name
|
182
|
+
# or lower level part of composite type
|
183
|
+
else
|
184
|
+
case previous_level_argument_metadata[data_level - 1][:data_type]
|
185
|
+
when 'PL/SQL RECORD'
|
186
|
+
previous_level_argument_metadata[data_level - 1][:fields][argument_name.downcase.to_sym] = argument_metadata
|
187
|
+
when 'PL/SQL TABLE', 'TABLE', 'VARRAY', 'REF CURSOR'
|
188
|
+
previous_level_argument_metadata[data_level - 1][:element] = argument_metadata
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
# if procedure is without arguments then create default empty argument list for default overload
|
194
|
+
@arguments[0] = {} if @arguments.keys.empty?
|
195
|
+
|
196
|
+
construct_argument_list_for_overloads
|
197
|
+
end
|
198
|
+
|
199
|
+
def construct_argument_list_for_overloads #:nodoc:
|
200
|
+
@overloads = @arguments.keys.sort
|
201
|
+
@overloads.each do |overload|
|
202
|
+
@argument_list[overload] = @arguments[overload].keys.sort {|k1, k2| @arguments[overload][k1][:position] <=> @arguments[overload][k2][:position]}
|
203
|
+
@out_list[overload] = @argument_list[overload].select {|k| @arguments[overload][k][:in_out] =~ /OUT/}
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def ensure_tmp_tables_created(overload) #:nodoc:
|
208
|
+
return if @tmp_tables_created.nil? || @tmp_tables_created[overload]
|
209
|
+
@tmp_table_names[overload] && @tmp_table_names[overload].each do |table_name, argument_metadata|
|
210
|
+
sql = "CREATE GLOBAL TEMPORARY TABLE #{table_name} (\n"
|
211
|
+
element_metadata = argument_metadata[:element]
|
212
|
+
case element_metadata[:data_type]
|
213
|
+
when 'PL/SQL RECORD'
|
214
|
+
fields_metadata = element_metadata[:fields]
|
215
|
+
fields_sorted_by_position = fields_metadata.keys.sort_by{|k| fields_metadata[k][:position]}
|
216
|
+
sql << fields_sorted_by_position.map do |field|
|
217
|
+
metadata = fields_metadata[field]
|
218
|
+
"#{field} #{ProcedureCommon.type_to_sql(metadata)}"
|
219
|
+
end.join(",\n")
|
220
|
+
else
|
221
|
+
sql << "element #{ProcedureCommon.type_to_sql(element_metadata)}"
|
222
|
+
end
|
223
|
+
sql << ",\ni__ NUMBER(38)\n"
|
224
|
+
sql << ") ON COMMIT PRESERVE ROWS\n"
|
225
|
+
sql_block = "DECLARE\nPRAGMA AUTONOMOUS_TRANSACTION;\nBEGIN\nEXECUTE IMMEDIATE :sql;\nEND;\n"
|
226
|
+
@schema.execute sql_block, sql
|
227
|
+
end
|
228
|
+
@tmp_tables_created[overload] = true
|
229
|
+
end
|
230
|
+
|
231
|
+
PLSQL_COMPOSITE_TYPES = ['PL/SQL RECORD', 'PL/SQL TABLE', 'TABLE', 'VARRAY', 'REF CURSOR'].freeze
|
232
|
+
def composite_type?(data_type) #:nodoc:
|
233
|
+
PLSQL_COMPOSITE_TYPES.include? data_type
|
234
|
+
end
|
235
|
+
|
236
|
+
PLSQL_COLLECTION_TYPES = ['PL/SQL TABLE', 'TABLE', 'VARRAY'].freeze
|
237
|
+
def collection_type?(data_type) #:nodoc:
|
238
|
+
PLSQL_COLLECTION_TYPES.include? data_type
|
239
|
+
end
|
240
|
+
|
241
|
+
def overloaded? #:nodoc:
|
242
|
+
@overloaded
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
class Procedure #:nodoc:
|
247
|
+
extend ProcedureClassMethods
|
248
|
+
include ProcedureCommon
|
249
|
+
|
250
|
+
attr_reader :arguments, :argument_list, :out_list, :return
|
251
|
+
attr_reader :schema, :schema_name, :package, :procedure
|
252
|
+
|
253
|
+
def initialize(schema, procedure, package, override_schema_name, object_id)
|
254
|
+
@schema = schema
|
255
|
+
@schema_name = override_schema_name || schema.schema_name
|
256
|
+
@procedure = procedure.to_s.upcase
|
257
|
+
@package = package
|
258
|
+
@object_id = object_id
|
259
|
+
|
260
|
+
get_argument_metadata
|
261
|
+
end
|
262
|
+
|
263
|
+
def exec(*args, &block)
|
264
|
+
call = ProcedureCall.new(self, args)
|
265
|
+
call.exec(&block)
|
266
|
+
end
|
267
|
+
|
268
|
+
end
|
269
|
+
end
|