ruby-plsql 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,345 @@
1
+ module PLSQL
2
+ class ProcedureCall #:nodoc:
3
+
4
+ def initialize(procedure, args = [])
5
+ @procedure = procedure
6
+ @overload = get_overload_from_arguments_list(args)
7
+ construct_sql(args)
8
+ end
9
+
10
+ def exec
11
+ @cursor = @procedure.schema.connection.parse(@sql)
12
+
13
+ @bind_values.each do |arg, value|
14
+ @cursor.bind_param(":#{arg}", value, @bind_metadata[arg])
15
+ end
16
+
17
+ @return_vars.each do |var|
18
+ @cursor.bind_param(":#{var}", nil, @return_vars_metadata[var])
19
+ end
20
+
21
+ @cursor.exec
22
+
23
+ if block_given?
24
+ yield get_return_value
25
+ nil
26
+ else
27
+ get_return_value
28
+ end
29
+ ensure
30
+ @cursor.close if @cursor
31
+ end
32
+
33
+ private
34
+
35
+ def get_overload_from_arguments_list(args)
36
+ # find which overloaded definition to use
37
+ # if definition is overloaded then match by number of arguments
38
+ if @procedure.overloaded?
39
+ # named arguments
40
+ if args.size == 1 && args[0].is_a?(Hash)
41
+ number_of_args = args[0].keys.size
42
+ overload = overload_argument_list.keys.detect do |ov|
43
+ overload_argument_list[ov].size == number_of_args &&
44
+ overload_arguments[ov].keys.sort_by{|k| k.to_s} == args[0].keys.sort_by{|k| k.to_s}
45
+ end
46
+ # sequential arguments
47
+ # TODO: should try to implement matching by types of arguments
48
+ else
49
+ number_of_args = args.size
50
+ overload = overload_argument_list.keys.detect do |ov|
51
+ overload_argument_list[ov].size == number_of_args
52
+ end
53
+ end
54
+ raise ArgumentError, "Wrong number of arguments passed to overloaded PL/SQL procedure" unless overload
55
+ overload
56
+ else
57
+ 0
58
+ end
59
+ end
60
+
61
+ def construct_sql(args)
62
+ @declare_sql = "DECLARE\n"
63
+ @assignment_sql = "BEGIN\n"
64
+ @call_sql = ""
65
+ @return_sql = ""
66
+ @return_vars = []
67
+ @return_vars_metadata = {}
68
+
69
+ # construct procedure call if procedure name is available
70
+ # otherwise will get surrounding call_sql from @procedure (used for table statements)
71
+ if procedure_name
72
+ @call_sql << add_return if return_metadata
73
+ @call_sql << "#{schema_name}." if schema_name
74
+ @call_sql << "#{package_name}." if package_name
75
+ @call_sql << "#{procedure_name}("
76
+ end
77
+
78
+ @bind_values = {}
79
+ @bind_metadata = {}
80
+
81
+ # Named arguments
82
+ if args.size == 1 && args[0].is_a?(Hash) &&
83
+ # do not use named arguments if procedure has just one PL/SQL record or object type argument -
84
+ # in that case passed Hash should be used as value for this PL/SQL record argument
85
+ # (which will be processed in sequential arguments bracnh)
86
+ !(argument_list.size == 1 &&
87
+ ['PL/SQL RECORD','OBJECT'].include?(arguments[(only_argument=argument_list[0])][:data_type]) &&
88
+ args[0].keys != [only_argument])
89
+ # Add missing output arguments with nil value
90
+ arguments.each do |arg, metadata|
91
+ if !args[0].has_key?(arg) && metadata[:in_out] == 'OUT'
92
+ args[0][arg] = nil
93
+ end
94
+ end
95
+ # Add passed parameters to procedure call with parameter names
96
+ @call_sql << args[0].map do |arg, value|
97
+ "#{arg} => " << add_argument(arg, value)
98
+ end.join(', ')
99
+
100
+ # Sequential arguments
101
+ else
102
+ argument_count = argument_list.size
103
+ raise ArgumentError, "Too many arguments passed to PL/SQL procedure" if args.size > argument_count
104
+ # Add missing output arguments with nil value
105
+ if args.size < argument_count &&
106
+ (args.size...argument_count).all?{|i| arguments[argument_list[i]][:in_out] == 'OUT'}
107
+ args += [nil] * (argument_count - args.size)
108
+ end
109
+ # Add passed parameters to procedure call in sequence
110
+ @call_sql << (0...args.size).map do |i|
111
+ arg = argument_list[i]
112
+ value = args[i]
113
+ add_argument(arg, value)
114
+ end.join(', ')
115
+ end
116
+
117
+ # finish procedure call construction if procedure name is available
118
+ # otherwise will get surrounding call_sql from @procedure (used for table statements)
119
+ if procedure_name
120
+ @call_sql << ");\n"
121
+ else
122
+ @call_sql = @procedure.call_sql(@call_sql)
123
+ end
124
+ add_out_vars
125
+ @sql = "" << @declare_sql << @assignment_sql << @call_sql << @return_sql << "END;\n"
126
+ # puts "DEBUG: sql = #{@sql.gsub "\n", "<br/>\n"}"
127
+ end
128
+
129
+ def add_argument(argument, value)
130
+ argument_metadata = arguments[argument]
131
+ raise ArgumentError, "Wrong argument #{argument.inspect} passed to PL/SQL procedure" unless argument_metadata
132
+ case argument_metadata[:data_type]
133
+ when 'PL/SQL RECORD'
134
+ @declare_sql << record_declaration_sql(argument, argument_metadata)
135
+ record_assignment_sql, record_bind_values, record_bind_metadata =
136
+ record_assignment_sql_values_metadata(argument, argument_metadata, value)
137
+ @assignment_sql << record_assignment_sql
138
+ @bind_values.merge!(record_bind_values)
139
+ @bind_metadata.merge!(record_bind_metadata)
140
+ "l_#{argument}"
141
+ when 'PL/SQL BOOLEAN'
142
+ @declare_sql << "l_#{argument} BOOLEAN;\n"
143
+ @assignment_sql << "l_#{argument} := (:#{argument} = 1);\n"
144
+ @bind_values[argument] = value.nil? ? nil : (value ? 1 : 0)
145
+ @bind_metadata[argument] = argument_metadata.merge(:data_type => "NUMBER", :data_precision => 1)
146
+ "l_#{argument}"
147
+ else
148
+ @bind_values[argument] = value
149
+ @bind_metadata[argument] = argument_metadata
150
+ ":#{argument}"
151
+ end
152
+ end
153
+
154
+ def record_declaration_sql(argument, argument_metadata)
155
+ fields_metadata = argument_metadata[:fields]
156
+ sql = "TYPE t_#{argument} IS RECORD (\n"
157
+ fields_sorted_by_position = fields_metadata.keys.sort_by{|k| fields_metadata[k][:position]}
158
+ sql << fields_sorted_by_position.map do |field|
159
+ metadata = fields_metadata[field]
160
+ "#{field} #{type_to_sql(metadata)}"
161
+ end.join(",\n")
162
+ sql << ");\n"
163
+ sql << "l_#{argument} t_#{argument};\n"
164
+ end
165
+
166
+ def record_assignment_sql_values_metadata(argument, argument_metadata, record_value)
167
+ sql = ""
168
+ bind_values = {}
169
+ bind_metadata = {}
170
+ (record_value||{}).each do |key, value|
171
+ field = key.is_a?(Symbol) ? key : key.to_s.downcase.to_sym
172
+ metadata = argument_metadata[:fields][field]
173
+ raise ArgumentError, "Wrong field name #{key.inspect} passed to PL/SQL record argument #{argument.inspect}" unless metadata
174
+ bind_variable = :"#{argument}_f#{metadata[:position]}"
175
+ sql << "l_#{argument}.#{field} := :#{bind_variable};\n"
176
+ bind_values[bind_variable] = value
177
+ bind_metadata[bind_variable] = metadata
178
+ end
179
+ [sql, bind_values, bind_metadata]
180
+ end
181
+
182
+ def add_return
183
+ case return_metadata[:data_type]
184
+ when 'PL/SQL RECORD'
185
+ @declare_sql << record_declaration_sql('return', return_metadata)
186
+ return_metadata[:fields].each do |field, metadata|
187
+ bind_variable = :"return_f#{metadata[:position]}"
188
+ @return_vars << bind_variable
189
+ @return_vars_metadata[bind_variable] = metadata
190
+ @return_sql << ":#{bind_variable} := l_return.#{field};\n"
191
+ end
192
+ "l_return := "
193
+ when 'PL/SQL BOOLEAN'
194
+ @declare_sql << "l_return BOOLEAN;\n"
195
+ @declare_sql << "x_return NUMBER(1);\n"
196
+ @return_vars << :return
197
+ @return_vars_metadata[:return] = return_metadata.merge(:data_type => "NUMBER", :data_precision => 1)
198
+ @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" <<
199
+ ":return := x_return;\n"
200
+ "l_return := "
201
+ else
202
+ @return_vars << :return
203
+ @return_vars_metadata[:return] = return_metadata
204
+ ':return := '
205
+ end
206
+ end
207
+
208
+ def add_out_vars
209
+ out_list.each do |argument|
210
+ argument_metadata = arguments[argument]
211
+ case argument_metadata[:data_type]
212
+ when 'PL/SQL RECORD'
213
+ argument_metadata[:fields].each do |field, metadata|
214
+ bind_variable = :"#{argument}_o#{metadata[:position]}"
215
+ @return_vars << bind_variable
216
+ @return_vars_metadata[bind_variable] = metadata
217
+ @return_sql << ":#{bind_variable} := l_#{argument}.#{field};\n"
218
+ end
219
+ when 'PL/SQL BOOLEAN'
220
+ @declare_sql << "x_#{argument} NUMBER(1);\n"
221
+ bind_variable = :"o_#{argument}"
222
+ @return_vars << bind_variable
223
+ @return_vars_metadata[bind_variable] = argument_metadata.merge(:data_type => "NUMBER", :data_precision => 1)
224
+ @return_sql << "IF l_#{argument} IS NULL THEN\nx_#{argument} := NULL;\n" <<
225
+ "ELSIF l_#{argument} THEN\nx_#{argument} := 1;\nELSE\nx_#{argument} := 0;\nEND IF;\n" <<
226
+ ":#{bind_variable} := x_#{argument};\n"
227
+ end
228
+ end
229
+ end
230
+
231
+ def type_to_sql(metadata)
232
+ case metadata[:data_type]
233
+ when 'NUMBER'
234
+ precision, scale = metadata[:data_precision], metadata[:data_scale]
235
+ "NUMBER#{precision ? "(#{precision}#{scale ? ",#{scale}": ""})" : ""}"
236
+ when 'VARCHAR2', 'CHAR', 'NVARCHAR2', 'NCHAR'
237
+ length = metadata[:data_length]
238
+ if length && (char_used = metadata[:char_used])
239
+ length = "#{length} #{char_used == 'C' ? 'CHAR' : 'BYTE'}"
240
+ end
241
+ "#{metadata[:data_type]}#{length ? "(#{length})": ""}"
242
+ when 'TABLE', 'VARRAY', 'OBJECT'
243
+ metadata[:sql_type_name]
244
+ else
245
+ metadata[:data_type]
246
+ end
247
+ end
248
+
249
+ def get_return_value
250
+ # if function with output parameters
251
+ if return_metadata && out_list.size > 0
252
+ result = [function_return_value, {}]
253
+ out_list.each do |k|
254
+ result[1][k] = out_var_value(k)
255
+ end
256
+ # if function without output parameters
257
+ elsif return_metadata
258
+ result = function_return_value
259
+ # if procedure with output parameters
260
+ elsif out_list.size > 0
261
+ result = {}
262
+ out_list.each do |k|
263
+ result[k] = out_var_value(k)
264
+ end
265
+ # if procedure without output parameters
266
+ else
267
+ result = nil
268
+ end
269
+ result
270
+ end
271
+
272
+ def function_return_value
273
+ case return_metadata[:data_type]
274
+ when 'PL/SQL RECORD'
275
+ return_value = {}
276
+ return_metadata[:fields].each do |field, metadata|
277
+ bind_variable = :"return_f#{metadata[:position]}"
278
+ return_value[field] = @cursor[":#{bind_variable}"]
279
+ end
280
+ return_value
281
+ when 'PL/SQL BOOLEAN'
282
+ numeric_value = @cursor[':return']
283
+ numeric_value.nil? ? nil : numeric_value == 1
284
+ else
285
+ @cursor[':return']
286
+ end
287
+ end
288
+
289
+ def out_var_value(argument)
290
+ argument_metadata = arguments[argument]
291
+ case argument_metadata[:data_type]
292
+ when 'PL/SQL RECORD'
293
+ return_value = {}
294
+ argument_metadata[:fields].each do |field, metadata|
295
+ bind_variable = :"#{argument}_o#{metadata[:position]}"
296
+ return_value[field] = @cursor[":#{bind_variable}"]
297
+ end
298
+ return_value
299
+ when 'PL/SQL BOOLEAN'
300
+ numeric_value = @cursor[":o_#{argument}"]
301
+ numeric_value.nil? ? nil : numeric_value == 1
302
+ else
303
+ @cursor[":#{argument}"]
304
+ end
305
+ end
306
+
307
+ def overload_argument_list
308
+ @overload_argument_list ||= @procedure.argument_list
309
+ end
310
+
311
+ def overload_arguments
312
+ @overload_arguments ||= @procedure.arguments
313
+ end
314
+
315
+ def argument_list
316
+ @argument_list ||= overload_argument_list[@overload]
317
+ end
318
+
319
+ def arguments
320
+ @arguments ||= overload_arguments[@overload]
321
+ end
322
+
323
+ def return_metadata
324
+ @return_metadata ||= @procedure.return[@overload]
325
+ end
326
+
327
+ def out_list
328
+ @out_list ||= @procedure.out_list[@overload]
329
+ end
330
+
331
+ def schema_name
332
+ @schema_name ||= @procedure.schema_name
333
+ end
334
+
335
+ def package_name
336
+ @package_name ||= @procedure.package
337
+ end
338
+
339
+ def procedure_name
340
+ @procedure_name ||= @procedure.procedure
341
+ end
342
+
343
+ end
344
+
345
+ end
@@ -1,9 +1,11 @@
1
1
  module PLSQL
2
2
  class Schema
3
+ include SQLStatements
4
+
3
5
  @@schemas = {}
4
6
 
5
7
  class <<self
6
- def find_or_new(connection_alias)
8
+ def find_or_new(connection_alias) #:nodoc:
7
9
  connection_alias ||= :default
8
10
  if @@schemas[connection_alias]
9
11
  @@schemas[connection_alias]
@@ -14,21 +16,32 @@ module PLSQL
14
16
 
15
17
  end
16
18
 
17
- def initialize(raw_conn = nil, schema = nil, first = true)
19
+ def initialize(raw_conn = nil, schema = nil, first = true) #:nodoc:
18
20
  self.connection = raw_conn
19
21
  @schema_name = schema ? schema.to_s.upcase : nil
20
22
  @first = first
21
23
  end
22
24
 
25
+ # Returns connection wrapper object (this is not raw OCI8 or JDBC connection!)
23
26
  def connection
24
27
  @connection
25
28
  end
26
29
 
27
- def raw_connection=(raw_conn)
30
+ def raw_connection=(raw_conn) #:nodoc:
28
31
  @connection = raw_conn ? Connection.create(raw_conn) : nil
29
32
  reset_instance_variables
30
33
  end
31
34
 
35
+ # Set connection to OCI8 or JDBC connection:
36
+ #
37
+ # plsql.connection = OCI8.new(database_user, database_password, DATABASE_NAME)
38
+ #
39
+ # or
40
+ #
41
+ # plsql.connection = java.sql.DriverManager.getConnection(
42
+ # "jdbc:oracle:thin:@#{DATABASE_HOST}:#{DATABASE_PORT}:#{DATABASE_NAME}",
43
+ # database_user, database_password)
44
+ #
32
45
  def connection=(conn)
33
46
  if conn.is_a?(::PLSQL::Connection)
34
47
  @connection = conn
@@ -38,39 +51,31 @@ module PLSQL
38
51
  end
39
52
  end
40
53
 
54
+ # Set connection to current ActiveRecord connection (use in initializer file):
55
+ #
56
+ # plsql.activerecord_class = ActiveRecord::Base
57
+ #
41
58
  def activerecord_class=(ar_class)
42
59
  @connection = ar_class ? Connection.create(nil, ar_class) : nil
43
60
  reset_instance_variables
44
61
  end
45
-
62
+
63
+ # Disconnect from Oracle
46
64
  def logoff
47
- connection.logoff
65
+ @connection.logoff
48
66
  self.connection = nil
49
67
  end
50
68
 
69
+ # Current Oracle schema name
51
70
  def schema_name
52
71
  return nil unless connection
53
72
  @schema_name ||= select_first("SELECT SYS_CONTEXT('userenv','session_user') FROM dual")[0]
54
73
  end
55
74
 
56
- def select_first(sql, *bindvars)
57
- # cursor = connection.exec(sql, *bindvars)
58
- # result = cursor.fetch
59
- # cursor.close
60
- # result
61
- connection.select_first(sql, *bindvars)
62
- end
63
-
64
- def commit
65
- connection.commit
66
- end
67
-
68
- def rollback
69
- connection.rollback
70
- end
71
-
72
75
  # Set to :local or :utc
73
76
  @@default_timezone = nil
77
+
78
+ # Default timezone to which database values will be converted - :utc or :local
74
79
  def default_timezone
75
80
  @@default_timezone ||
76
81
  # Use ActiveRecord class default_timezone when ActiveRecord connection is used
@@ -78,7 +83,8 @@ module PLSQL
78
83
  # default to local timezone
79
84
  :local
80
85
  end
81
-
86
+
87
+ # Set default timezone to which database values will be converted - :utc or :local
82
88
  def default_timezone=(value)
83
89
  if [:local, :utc].include?(value)
84
90
  @@default_timezone = value
@@ -89,7 +95,7 @@ module PLSQL
89
95
 
90
96
  # Same implementation as for ActiveRecord
91
97
  # DateTimes aren't aware of DST rules, so use a consistent non-DST offset when creating a DateTime with an offset in the local zone
92
- def local_timezone_offset
98
+ def local_timezone_offset #:nodoc:
93
99
  ::Time.local(2007).utc_offset.to_r / 86400
94
100
  end
95
101
 
@@ -97,32 +103,35 @@ module PLSQL
97
103
 
98
104
  def reset_instance_variables
99
105
  if @connection
100
- @procedures = {}
101
- @packages = {}
102
- @schemas = {}
106
+ @schema_objects = {}
103
107
  else
104
- @procedures = nil
105
- @packages = nil
106
- @schemas = nil
108
+ @schema_objects = nil
107
109
  end
110
+ @schema_name = nil
108
111
  @@default_timezone = nil
109
112
  end
110
113
 
111
- def method_missing(method, *args)
114
+ def method_missing(method, *args, &block)
112
115
  raise ArgumentError, "No PL/SQL connection" unless connection
113
- if procedure = @procedures[method]
114
- procedure.exec(*args)
116
+ # look in cache at first
117
+ if schema_object = @schema_objects[method]
118
+ if schema_object.is_a?(Procedure)
119
+ schema_object.exec(*args, &block)
120
+ else
121
+ schema_object
122
+ end
123
+ # search in database
115
124
  elsif procedure = Procedure.find(self, method)
116
- @procedures[method] = procedure
117
- procedure.exec(*args)
118
- elsif package = @packages[method]
119
- package
125
+ @schema_objects[method] = procedure
126
+ procedure.exec(*args, &block)
120
127
  elsif package = Package.find(self, method)
121
- @packages[method] = package
122
- elsif schema = @schemas[method]
123
- schema
128
+ @schema_objects[method] = package
129
+ elsif table = Table.find(self, method)
130
+ @schema_objects[method] = table
131
+ elsif sequence = Sequence.find(self, method)
132
+ @schema_objects[method] = sequence
124
133
  elsif schema = find_other_schema(method)
125
- @schemas[method] = schema
134
+ @schema_objects[method] = schema
126
135
  else
127
136
  raise ArgumentError, "No PL/SQL procedure found"
128
137
  end
@@ -141,6 +150,15 @@ module PLSQL
141
150
  end
142
151
 
143
152
  module Kernel
153
+ # Returns current schema object. You can now chain either database object (packages, procedures, tables, sequences)
154
+ # in current schema or specify different schema name. Examples:
155
+ #
156
+ # plsql.test_function('some parameter')
157
+ # plsql.test_package.test_function('some parameter')
158
+ # plsql.other_schema.test_package.test_function('some parameter')
159
+ # plsql.table_name.all
160
+ # plsql.other_schema.table_name.all
161
+ #
144
162
  def plsql(connection_alias = nil)
145
163
  PLSQL::Schema.find_or_new(connection_alias)
146
164
  end