flash-gordons-ruby-plsql 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|