ruby-plsql 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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