ruby-plsql 0.9.9-java

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
+ module TableClassMethods # :nodoc:
3
+ def find(schema, table)
4
+ if schema.select_first(
5
+ "SELECT table_name FROM all_tables
6
+ WHERE owner = :owner
7
+ AND table_name = :table_name",
8
+ schema.schema_name, table.to_s.upcase)
9
+ new(schema, table)
10
+ # search for synonym
11
+ elsif (row = schema.select_first(
12
+ "SELECT t.owner, t.table_name
13
+ FROM all_synonyms s, all_tables t
14
+ WHERE s.owner = :owner
15
+ AND s.synonym_name = :synonym_name
16
+ AND t.owner = s.table_owner
17
+ AND t.table_name = s.table_name
18
+ UNION ALL
19
+ SELECT t.owner, t.table_name
20
+ FROM all_synonyms s, all_tables t
21
+ WHERE s.owner = 'PUBLIC'
22
+ AND s.synonym_name = :synonym_name
23
+ AND t.owner = s.table_owner
24
+ AND t.table_name = s.table_name",
25
+ schema.schema_name, table.to_s.upcase, table.to_s.upcase))
26
+ new(schema, row[1], row[0])
27
+ else
28
+ nil
29
+ end
30
+ end
31
+ end
32
+
33
+ class Table
34
+ extend TableClassMethods
35
+
36
+ attr_reader :columns, :schema_name, :table_name # :nodoc:
37
+
38
+ def initialize(schema, table, override_schema_name = nil) # :nodoc:
39
+ @schema = schema
40
+ @schema_name = override_schema_name || schema.schema_name
41
+ @table_name = table.to_s.upcase
42
+ @columns = {}
43
+
44
+ @schema.select_all(
45
+ "SELECT c.column_name, c.column_id position,
46
+ c.data_type, c.data_length, c.data_precision, c.data_scale, c.char_used,
47
+ c.data_type_owner, c.data_type_mod,
48
+ CASE WHEN c.data_type_owner IS NULL THEN NULL
49
+ ELSE (SELECT t.typecode FROM all_types t
50
+ WHERE t.owner = c.data_type_owner
51
+ AND t.type_name = c.data_type) END typecode,
52
+ c.nullable, c.data_default
53
+ FROM all_tab_columns c
54
+ WHERE c.owner = :owner
55
+ AND c.table_name = :table_name",
56
+ @schema_name, @table_name
57
+ ) do |r|
58
+ column_name, position,
59
+ data_type, data_length, data_precision, data_scale, char_used,
60
+ data_type_owner, _, typecode, nullable, data_default = r
61
+ # remove scale (n) from data_type (returned for TIMESTAMPs and INTERVALs)
62
+ data_type.sub!(/\(\d+\)/, "")
63
+ # store column metadata
64
+ @columns[column_name.downcase.to_sym] = {
65
+ position: position && position.to_i,
66
+ data_type: data_type_owner && (typecode == "COLLECTION" ? "TABLE" : "OBJECT") || data_type,
67
+ data_length: data_type_owner ? nil : data_length && data_length.to_i,
68
+ data_precision: data_precision && data_precision.to_i,
69
+ data_scale: data_scale && data_scale.to_i,
70
+ char_used: char_used,
71
+ type_owner: data_type_owner,
72
+ type_name: data_type_owner && data_type,
73
+ sql_type_name: data_type_owner && "#{data_type_owner}.#{data_type}",
74
+ nullable: nullable == "Y", # store as true or false
75
+ data_default: data_default && data_default.strip # remove leading and trailing whitespace
76
+ }
77
+ end
78
+ end
79
+
80
+ # list of table column names
81
+ def column_names
82
+ @column_names ||= @columns.keys.sort_by { |k| columns[k][:position] }
83
+ end
84
+
85
+ # General select method with :first, :all or :count as first parameter.
86
+ # It is recommended to use #first, #all or #count method instead of this one.
87
+ def select(first_or_all, sql_params = "", *bindvars)
88
+ case first_or_all
89
+ when :first, :all
90
+ select_sql = +"SELECT * "
91
+ when :count
92
+ select_sql = +"SELECT COUNT(*) "
93
+ else
94
+ raise ArgumentError, "Only :first, :all or :count are supported"
95
+ end
96
+ select_sql << "FROM \"#{@schema_name}\".\"#{@table_name}\" "
97
+ case sql_params
98
+ when String
99
+ select_sql << sql_params
100
+ when Hash
101
+ raise ArgumentError, "Cannot specify bind variables when passing WHERE conditions as Hash" unless bindvars.empty?
102
+ where_sqls = []
103
+ order_by_sql = nil
104
+ sql_params.each do |k, v|
105
+ if k == :order_by
106
+ order_by_sql = " ORDER BY #{v} "
107
+ elsif v.nil? || v == :is_null
108
+ where_sqls << "#{k} IS NULL"
109
+ elsif v == :is_not_null
110
+ where_sqls << "#{k} IS NOT NULL"
111
+ else
112
+ where_sqls << "#{k} = :#{k}"
113
+ bindvars << v
114
+ end
115
+ end
116
+ select_sql << "WHERE " << where_sqls.join(" AND ") unless where_sqls.empty?
117
+ select_sql << order_by_sql if order_by_sql
118
+ else
119
+ raise ArgumentError, "Only String or Hash can be provided as SQL condition argument"
120
+ end
121
+ if first_or_all == :count
122
+ @schema.select_one(select_sql, *bindvars)
123
+ else
124
+ @schema.select(first_or_all, select_sql, *bindvars)
125
+ end
126
+ end
127
+
128
+ # Select all table records using optional conditions. Examples:
129
+ #
130
+ # plsql.employees.all
131
+ # plsql.employees.all(:order_by => :employee_id)
132
+ # plsql.employees.all("WHERE employee_id > :employee_id", 5)
133
+ #
134
+ def all(sql = "", *bindvars)
135
+ select(:all, sql, *bindvars)
136
+ end
137
+
138
+ # Select first table record using optional conditions. Examples:
139
+ #
140
+ # plsql.employees.first
141
+ # plsql.employees.first(:employee_id => 1)
142
+ # plsql.employees.first("WHERE employee_id = 1")
143
+ # plsql.employees.first("WHERE employee_id = :employee_id", 1)
144
+ #
145
+ def first(sql = "", *bindvars)
146
+ select(:first, sql, *bindvars)
147
+ end
148
+
149
+ # Count table records using optional conditions. Examples:
150
+ #
151
+ # plsql.employees.count
152
+ # plsql.employees.count("WHERE employee_id > :employee_id", 5)
153
+ #
154
+ def count(sql = "", *bindvars)
155
+ select(:count, sql, *bindvars)
156
+ end
157
+
158
+ # Insert record or records in table. Examples:
159
+ #
160
+ # employee = { :employee_id => 1, :first_name => 'First', :last_name => 'Last', :hire_date => Time.local(2000,01,31) }
161
+ # plsql.employees.insert employee
162
+ # # => INSERT INTO employees VALUES (1, 'First', 'Last', ...)
163
+ #
164
+ # employees = [employee1, employee2, ... ] # array of many Hashes
165
+ # plsql.employees.insert employees
166
+ #
167
+ def insert(record)
168
+ # if Array of records is passed then insert each individually
169
+ if record.is_a?(Array)
170
+ record.each { |r| insert(r) }
171
+ return nil
172
+ end
173
+
174
+ table_proc = TableProcedure.new(@schema, self, :insert)
175
+ record = record.map { |k, v| [k.downcase.to_sym, v] }.to_h
176
+ table_proc.add_insert_arguments(record)
177
+
178
+ call = ProcedureCall.new(table_proc, table_proc.argument_values)
179
+ call.exec
180
+ end
181
+
182
+ # Insert record or records in table using array of values. Examples:
183
+ #
184
+ # # with values for all columns
185
+ # plsql.employees.insert_values [1, 'First', 'Last', Time.local(2000,01,31)]
186
+ # # => INSERT INTO employees VALUES (1, 'First', 'Last', ...)
187
+ #
188
+ # # with values for specified columns
189
+ # plsql.employees.insert_values [:employee_id, :first_name, :last_name], [1, 'First', 'Last']
190
+ # # => INSERT INTO employees (employee_id, first_name, last_name) VALUES (1, 'First', 'Last')
191
+ #
192
+ # # with values for many records
193
+ # plsql.employees.insert_values [:employee_id, :first_name, :last_name], [1, 'First', 'Last'], [2, 'Second', 'Last']
194
+ # # => INSERT INTO employees (employee_id, first_name, last_name) VALUES (1, 'First', 'Last')
195
+ # # => INSERT INTO employees (employee_id, first_name, last_name) VALUES (2, 'Second', 'Last')
196
+ #
197
+ def insert_values(*args)
198
+ raise ArgumentError, "no arguments given" unless args.first
199
+ # if first argument is array of symbols then use it as list of fields
200
+ if args.first.all? { |a| a.instance_of?(Symbol) }
201
+ fields = args.shift
202
+ # otherwise use all columns as list of fields
203
+ else
204
+ fields = column_names
205
+ end
206
+ args.each do |record|
207
+ raise ArgumentError, "record should be Array of values" unless record.is_a?(Array)
208
+ raise ArgumentError, "wrong number of column values" unless record.size == fields.size
209
+ insert(ArrayHelpers::to_hash(fields, record))
210
+ end
211
+ end
212
+
213
+ # Update table records using optional conditions. Example:
214
+ #
215
+ # plsql.employees.update(:first_name => 'Second', :where => {:employee_id => 1})
216
+ # # => UPDATE employees SET first_name = 'Second' WHERE employee_id = 1
217
+ #
218
+ def update(params)
219
+ raise ArgumentError, "Only Hash parameter can be passed to table update method" unless params.is_a?(Hash)
220
+ where = params.delete(:where)
221
+
222
+ table_proc = TableProcedure.new(@schema, self, :update)
223
+ table_proc.add_set_arguments(params)
224
+ table_proc.add_where_arguments(where) if where
225
+ call = ProcedureCall.new(table_proc, table_proc.argument_values)
226
+ call.exec
227
+ end
228
+
229
+ # Delete table records using optional conditions. Example:
230
+ #
231
+ # plsql.employees.delete(:employee_id => 1)
232
+ # # => DELETE FROM employees WHERE employee_id = 1
233
+ #
234
+ def delete(sql_params = "", *bindvars)
235
+ delete_sql = "DELETE FROM \"#{@schema_name}\".\"#{@table_name}\" "
236
+ case sql_params
237
+ when String
238
+ delete_sql << sql_params
239
+ when Hash
240
+ raise ArgumentError, "Cannot specify bind variables when passing WHERE conditions as Hash" unless bindvars.empty?
241
+ where_sqls = []
242
+ sql_params.each do |k, v|
243
+ where_sqls << "#{k} = :#{k}"
244
+ bindvars << v
245
+ end
246
+ delete_sql << "WHERE " << where_sqls.join(" AND ") unless where_sqls.empty?
247
+ else
248
+ raise ArgumentError, "Only String or Hash can be provided as SQL condition argument"
249
+ end
250
+ @schema.execute(delete_sql, *bindvars)
251
+ end
252
+
253
+ # wrapper class to simulate Procedure class for ProcedureClass#exec
254
+ class TableProcedure # :nodoc:
255
+ attr_reader :arguments, :argument_list, :return, :out_list, :schema
256
+
257
+ def initialize(schema, table, operation)
258
+ @schema = schema
259
+ @table = table
260
+ @operation = operation
261
+
262
+ @return = [nil]
263
+ @out_list = [[]]
264
+
265
+ case @operation
266
+ when :insert
267
+ @argument_list = [[]]
268
+ @arguments = [{}]
269
+ @insert_columns = []
270
+ @insert_values = []
271
+ when :update
272
+ @argument_list = [[]]
273
+ @arguments = [{}]
274
+ @set_sqls = []
275
+ @set_values = []
276
+ @where_sqls = []
277
+ @where_values = []
278
+ end
279
+ end
280
+
281
+ def overloaded?
282
+ false
283
+ end
284
+
285
+ def procedure
286
+ nil
287
+ end
288
+
289
+ def add_insert_arguments(params)
290
+ params.each do |k, v|
291
+ raise ArgumentError, "Invalid column name #{k.inspect} specified as argument" unless (column_metadata = @table.columns[k])
292
+ @argument_list[0] << k
293
+ @arguments[0][k] = column_metadata
294
+ @insert_values << v
295
+ end
296
+ end
297
+
298
+ def add_set_arguments(params)
299
+ params.each do |k, v|
300
+ raise ArgumentError, "Invalid column name #{k.inspect} specified as argument" unless (column_metadata = @table.columns[k])
301
+ @argument_list[0] << k
302
+ @arguments[0][k] = column_metadata
303
+ @set_sqls << "#{k}=:#{k}"
304
+ @set_values << v
305
+ end
306
+ end
307
+
308
+ def add_where_arguments(params)
309
+ case params
310
+ when Hash
311
+ params.each do |k, v|
312
+ raise ArgumentError, "Invalid column name #{k.inspect} specified as argument" unless (column_metadata = @table.columns[k])
313
+ @argument_list[0] << :"w_#{k}"
314
+ @arguments[0][:"w_#{k}"] = column_metadata
315
+ @where_sqls << "#{k}=:w_#{k}"
316
+ @where_values << v
317
+ end
318
+ when String
319
+ @where_sqls << params
320
+ end
321
+ end
322
+
323
+ def argument_values
324
+ case @operation
325
+ when :insert
326
+ @insert_values
327
+ when :update
328
+ @set_values + @where_values
329
+ end
330
+ end
331
+
332
+ def call_sql(params_string)
333
+ case @operation
334
+ when :insert
335
+ "INSERT INTO \"#{@table.schema_name}\".\"#{@table.table_name}\"(#{@argument_list[0].map { |a| a.to_s }.join(', ')}) VALUES (#{params_string});\n"
336
+ when :update
337
+ update_sql = "UPDATE \"#{@table.schema_name}\".\"#{@table.table_name}\" SET #{@set_sqls.join(', ')}"
338
+ update_sql << " WHERE #{@where_sqls.join(' AND ')}" unless @where_sqls.empty?
339
+ update_sql << ";\n"
340
+ update_sql
341
+ end
342
+ end
343
+ end
344
+ end
345
+ end
data/lib/plsql/type.rb ADDED
@@ -0,0 +1,270 @@
1
+ module PLSQL
2
+ module TypeClassMethods # :nodoc:
3
+ def find(schema, type)
4
+ if schema.select_first(
5
+ "SELECT type_name FROM all_types
6
+ WHERE owner = :owner
7
+ AND type_name = :table_name",
8
+ schema.schema_name, type.to_s.upcase)
9
+ new(schema, type)
10
+ # search for synonym
11
+ elsif (row = schema.select_first(
12
+ "SELECT t.owner, t.type_name
13
+ FROM all_synonyms s, all_types t
14
+ WHERE s.owner = :owner
15
+ AND s.synonym_name = :synonym_name
16
+ AND t.owner = s.table_owner
17
+ AND t.type_name = s.table_name
18
+ UNION ALL
19
+ SELECT t.owner, t.type_name
20
+ FROM all_synonyms s, all_types t
21
+ WHERE s.owner = 'PUBLIC'
22
+ AND s.synonym_name = :synonym_name
23
+ AND t.owner = s.table_owner
24
+ AND t.type_name = s.table_name",
25
+ schema.schema_name, type.to_s.upcase, type.to_s.upcase))
26
+ new(schema, row[1], row[0])
27
+ else
28
+ nil
29
+ end
30
+ end
31
+ end
32
+
33
+ class Type
34
+ extend TypeClassMethods
35
+
36
+ attr_reader :typecode, :attributes, :schema_name, :type_name, :type_object_id # :nodoc:
37
+
38
+ def initialize(schema, type, override_schema_name = nil) # :nodoc:
39
+ @schema = schema
40
+ @schema_name = override_schema_name || schema.schema_name
41
+ @type_name = type.to_s.upcase
42
+ @attributes = {}
43
+ @type_procedures = {}
44
+
45
+ @typecode, @type_object_id = @schema.select_first(
46
+ "SELECT t.typecode, o.object_id FROM all_types t, all_objects o
47
+ WHERE t.owner = :owner
48
+ AND t.type_name = :type_name
49
+ AND o.owner = t.owner
50
+ AND o.object_name = t.type_name
51
+ AND o.object_type = 'TYPE'",
52
+ @schema_name, @type_name)
53
+
54
+ @schema.select_all(
55
+ "SELECT attr_name, attr_no,
56
+ attr_type_name, length, precision, scale,
57
+ attr_type_owner, attr_type_mod,
58
+ (SELECT t.typecode FROM all_types t
59
+ WHERE t.owner = attr_type_owner
60
+ AND t.type_name = attr_type_name) typecode
61
+ FROM all_type_attrs
62
+ WHERE owner = :owner
63
+ AND type_name = :type_name
64
+ ORDER BY attr_no",
65
+ @schema_name, @type_name
66
+ ) do |r|
67
+ attr_name, position,
68
+ data_type, data_length, data_precision, data_scale,
69
+ data_type_owner, _, typecode = r
70
+ @attributes[attr_name.downcase.to_sym] = {
71
+ position: position && position.to_i,
72
+ data_type: data_type_owner && (typecode == "COLLECTION" ? "TABLE" : "OBJECT") || data_type,
73
+ data_length: data_type_owner ? nil : data_length && data_length.to_i,
74
+ data_precision: data_precision && data_precision.to_i,
75
+ data_scale: data_scale && data_scale.to_i,
76
+ type_owner: data_type_owner,
77
+ type_name: data_type_owner && data_type,
78
+ sql_type_name: data_type_owner && "#{data_type_owner}.#{data_type}"
79
+ }
80
+ end
81
+ end
82
+
83
+ # is type collection?
84
+ def collection?
85
+ @is_collection ||= @typecode == "COLLECTION"
86
+ end
87
+
88
+ # list of object type attribute names
89
+ def attribute_names
90
+ @attribute_names ||= @attributes.keys.sort_by { |k| @attributes[k][:position] }
91
+ end
92
+
93
+ # create new PL/SQL object instance
94
+ def new(*args, &block)
95
+ procedure = find_procedure(:new)
96
+ # in case of collections pass array of elements as one argument for constructor
97
+ if collection? && !(args.size == 1 && args[0].is_a?(Array))
98
+ args = [args]
99
+ end
100
+ result = procedure.exec_with_options(args, { skip_self: true }, &block)
101
+ # TODO: collection constructor should return Array of ObhjectInstance objects
102
+ if collection?
103
+ result
104
+ else
105
+ # TODO: what to do if block is passed to constructor?
106
+ ObjectInstance.create(self, result)
107
+ end
108
+ end
109
+
110
+ def method_missing(method, *args, &block) # :nodoc:
111
+ if procedure = find_procedure(method)
112
+ procedure.exec_with_options(args, {}, &block)
113
+ else
114
+ raise ArgumentError, "No PL/SQL procedure '#{method.to_s.upcase}' found for type '#{@type_name}'"
115
+ end
116
+ end
117
+
118
+ def find_procedure(new_or_procedure) # :nodoc:
119
+ @type_procedures[new_or_procedure] ||= begin
120
+ procedure_name = new_or_procedure == :new ? @type_name : new_or_procedure
121
+ # find defined procedure for type
122
+ if @schema.select_first(
123
+ "SELECT procedure_name FROM all_procedures
124
+ WHERE owner = :owner
125
+ AND object_name = :object_name
126
+ AND procedure_name = :procedure_name",
127
+ @schema_name, @type_name, procedure_name.to_s.upcase)
128
+ TypeProcedure.new(@schema, self, procedure_name)
129
+ # call default constructor
130
+ elsif new_or_procedure == :new
131
+ TypeProcedure.new(@schema, self, :new)
132
+ end
133
+ end
134
+ end
135
+
136
+ # wrapper class to simulate Procedure class for ProcedureClass#exec
137
+ class TypeProcedure # :nodoc:
138
+ include ProcedureCommon
139
+
140
+ def initialize(schema, type, procedure)
141
+ @schema = schema
142
+ @type = type
143
+ @schema_name = @type.schema_name
144
+ @type_name = @type.type_name
145
+ @object_id = @type.type_object_id
146
+
147
+ # if default constructor
148
+ if @default_constructor = (procedure == :new)
149
+ @procedure = @type.collection? ? nil : @type_name
150
+ set_default_constructor_arguments
151
+ # if defined type procedure
152
+ else
153
+ @procedure = procedure.to_s.upcase
154
+ get_argument_metadata
155
+ # add also definition for default constructor in case of custom constructor
156
+ set_default_constructor_arguments if @procedure == @type_name
157
+ end
158
+
159
+ # constructors do not need type prefix in call
160
+ @package = @procedure == @type_name ? nil : @type_name
161
+ end
162
+
163
+ # will be called for collection constructor
164
+ def call_sql(params_string)
165
+ "#{params_string};\n"
166
+ end
167
+
168
+ attr_reader :arguments, :argument_list, :out_list
169
+ def arguments_without_self
170
+ @arguments_without_self ||= begin
171
+ hash = {}
172
+ @arguments.each do |ov, args|
173
+ hash[ov] = args.reject { |key, value| key == :self }
174
+ end
175
+ hash
176
+ end
177
+ end
178
+
179
+ def argument_list_without_self
180
+ @argument_list_without_self ||= begin
181
+ hash = {}
182
+ @argument_list.each do |ov, arg_list|
183
+ hash[ov] = arg_list.select { |arg| arg != :self }
184
+ end
185
+ hash
186
+ end
187
+ end
188
+
189
+ def out_list_without_self
190
+ @out_list_without_self ||= begin
191
+ hash = {}
192
+ @out_list.each do |ov, out_list|
193
+ hash[ov] = out_list.select { |arg| arg != :self }
194
+ end
195
+ hash
196
+ end
197
+ end
198
+
199
+ def exec_with_options(args, options = {}, &block)
200
+ call = ProcedureCall.new(self, args, options)
201
+ result = call.exec(&block)
202
+ # if procedure was called then modified object is returned in SELF output parameter
203
+ if result.is_a?(Hash) && result[:self]
204
+ object = result.delete(:self)
205
+ result.empty? ? object : [object, result]
206
+ else
207
+ result
208
+ end
209
+ end
210
+
211
+ private
212
+
213
+ def set_default_constructor_arguments
214
+ @arguments ||= {}
215
+ @argument_list ||= {}
216
+ @out_list ||= {}
217
+ @return ||= {}
218
+ # either this will be the only overload or it will be additional
219
+ overload = @arguments.keys.size
220
+ # if type is collection then expect array of objects as argument
221
+ if @type.collection?
222
+ @arguments[overload] = {
223
+ value: {
224
+ position: 1,
225
+ data_type: "TABLE",
226
+ in_out: "IN",
227
+ type_owner: @schema_name,
228
+ type_name: @type_name,
229
+ sql_type_name: "#{@schema_name}.#{@type_name}"
230
+ }
231
+ }
232
+ # otherwise if type is object type then expect object attributes as argument list
233
+ else
234
+ @arguments[overload] = @type.attributes
235
+ end
236
+ attributes = @arguments[overload]
237
+ @argument_list[overload] = attributes.keys.sort { |k1, k2| attributes[k1][:position] <=> attributes[k2][:position] }
238
+ # returns object or collection
239
+ @return[overload] = {
240
+ position: 0,
241
+ data_type: @type.collection? ? "TABLE" : "OBJECT",
242
+ in_out: "OUT",
243
+ type_owner: @schema_name,
244
+ type_name: @type_name,
245
+ sql_type_name: "#{@schema_name}.#{@type_name}"
246
+ }
247
+ @out_list[overload] = []
248
+ @overloaded = overload > 0
249
+ end
250
+ end
251
+ end
252
+
253
+ class ObjectInstance < Hash # :nodoc:
254
+ attr_accessor :plsql_type
255
+
256
+ def self.create(type, attributes)
257
+ object = self.new.merge!(attributes)
258
+ object.plsql_type = type
259
+ object
260
+ end
261
+
262
+ def method_missing(method, *args, &block)
263
+ if procedure = @plsql_type.find_procedure(method)
264
+ procedure.exec_with_options(args, self: self, &block)
265
+ else
266
+ raise ArgumentError, "No PL/SQL procedure '#{method.to_s.upcase}' found for type '#{@plsql_type.type_name}' object"
267
+ end
268
+ end
269
+ end
270
+ end