ruby-plsql 0.4.1 → 0.4.2

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.
data/lib/plsql/schema.rb CHANGED
@@ -25,6 +25,10 @@ module PLSQL
25
25
  # Returns connection wrapper object (this is not raw OCI8 or JDBC connection!)
26
26
  attr_reader :connection
27
27
 
28
+ def root_schema #:nodoc:
29
+ @original_schema || self
30
+ end
31
+
28
32
  def raw_connection=(raw_conn) #:nodoc:
29
33
  @connection = raw_conn ? Connection.create(raw_conn) : nil
30
34
  reset_instance_variables
@@ -32,16 +36,16 @@ module PLSQL
32
36
 
33
37
  # Set connection to OCI8 or JDBC connection:
34
38
  #
35
- # plsql.connection = OCI8.new(database_user, database_password, DATABASE_NAME)
39
+ # plsql.connection = OCI8.new(database_user, database_password, database_name)
36
40
  #
37
41
  # or
38
42
  #
39
43
  # plsql.connection = java.sql.DriverManager.getConnection(
40
- # "jdbc:oracle:thin:@#{DATABASE_HOST}:#{DATABASE_PORT}:#{DATABASE_NAME}",
44
+ # "jdbc:oracle:thin:@#{database_host}:#{database_port}:#{database_name}",
41
45
  # database_user, database_password)
42
46
  #
43
47
  def connection=(conn)
44
- if conn.is_a?(::PLSQL::Connection)
48
+ if conn.is_a?(Connection)
45
49
  @connection = conn
46
50
  reset_instance_variables
47
51
  else
@@ -50,6 +54,23 @@ module PLSQL
50
54
  conn
51
55
  end
52
56
 
57
+ # Create new OCI8 or JDBC connection using one of the following ways:
58
+ #
59
+ # plsql.connect! username, password, database_tns_alias
60
+ # plsql.connect! username, password, :host => host, :port => port, :database => database
61
+ # plsql.connect! :username => username, :password => password, :database => database_tns_alias
62
+ # plsql.connect! :username => username, :password => password, :host => host, :port => port, :database => database
63
+ #
64
+ def connect!(*args)
65
+ params = {}
66
+ params[:username] = args.shift if args[0].is_a?(String)
67
+ params[:password] = args.shift if args[0].is_a?(String)
68
+ params[:database] = args.shift if args[0].is_a?(String)
69
+ params.merge!(args.shift) if args[0].is_a?(Hash)
70
+ raise ArgumentError, "Wrong number of arguments" unless args.empty?
71
+ self.connection = Connection.create_new(params)
72
+ end
73
+
53
74
  # Set connection to current ActiveRecord connection (use in initializer file):
54
75
  #
55
76
  # plsql.activerecord_class = ActiveRecord::Base
@@ -156,12 +177,14 @@ module PLSQL
156
177
  raise ArgumentError, "No database connection" unless connection
157
178
  # search in database if not in cache at first
158
179
  object = (@schema_objects[method] ||= find_database_object(method) || find_other_schema(method) ||
159
- find_public_synonym(method)) || find_standard_procedure(method)
180
+ find_public_synonym(method) || find_standard_procedure(method))
160
181
 
161
182
  raise ArgumentError, "No database object '#{method.to_s.upcase}' found" unless object
162
183
 
163
184
  if object.is_a?(Procedure)
164
185
  object.exec(*args, &block)
186
+ elsif object.is_a?(Type) && !args.empty?
187
+ object.new(*args, &block)
165
188
  else
166
189
  object
167
190
  end
@@ -171,14 +194,24 @@ module PLSQL
171
194
  object_schema_name = override_schema_name || schema_name
172
195
  object_name = name.to_s.upcase
173
196
  if row = select_first(
174
- "SELECT object_type, object_id FROM all_objects
197
+ "SELECT o.object_type, o.object_id, o.status,
198
+ (CASE WHEN o.object_type = 'PACKAGE'
199
+ THEN (SELECT ob.status FROM all_objects ob
200
+ WHERE ob.owner = o.owner AND ob.object_name = o.object_name AND ob.object_type = 'PACKAGE BODY')
201
+ ELSE NULL END) body_status
202
+ FROM all_objects o
175
203
  WHERE owner = :owner AND object_name = :object_name
176
204
  AND object_type IN ('PROCEDURE','FUNCTION','PACKAGE','TABLE','VIEW','SEQUENCE','TYPE','SYNONYM')",
177
205
  object_schema_name, object_name)
178
- case row[0]
206
+ object_type, object_id, status, body_status = row
207
+ raise ArgumentError, "Database object '#{object_schema_name}.#{object_name}' is not in valid status\n" <<
208
+ _errors(object_schema_name, object_name, object_type) if status == 'INVALID'
209
+ raise ArgumentError, "Package '#{object_schema_name}.#{object_name}' body is not in valid status\n" <<
210
+ _errors(object_schema_name, object_name, 'PACKAGE BODY') if body_status == 'INVALID'
211
+ case object_type
179
212
  when 'PROCEDURE', 'FUNCTION'
180
- Procedure.new(self, name, nil, override_schema_name, row[1])
181
- when 'PACKAGE', 'PACKAGE BODY'
213
+ Procedure.new(self, name, nil, override_schema_name, object_id)
214
+ when 'PACKAGE'
182
215
  Package.new(self, name, override_schema_name)
183
216
  when 'TABLE'
184
217
  Table.new(self, name, override_schema_name)
@@ -195,6 +228,24 @@ module PLSQL
195
228
  end
196
229
  end
197
230
 
231
+ def _errors(object_schema_name, object_name, object_type)
232
+ result = ""
233
+ previous_line = 0
234
+ select_all(
235
+ "SELECT e.line, e.position, e.text error_text, s.text source_text
236
+ FROM all_errors e, all_source s
237
+ WHERE e.owner = :owner AND e.name = :name AND e.type = :type
238
+ AND s.owner = e.owner AND s.name = e.name AND s.type = e.type AND s.line = e.line
239
+ ORDER BY e.sequence",
240
+ object_schema_name, object_name, object_type
241
+ ).each do |line, position, error_text, source_text|
242
+ result << "Error on line #{'%4d' % line}: #{source_text}" if line > previous_line
243
+ result << " position #{'%4d' % position}: #{error_text}\n"
244
+ previous_line = line
245
+ end
246
+ result unless result.empty?
247
+ end
248
+
198
249
  def find_other_schema(name)
199
250
  return nil if @original_schema
200
251
  if select_first("SELECT username FROM all_users WHERE username = :username", name.to_s.upcase)
@@ -61,6 +61,27 @@ module PLSQL
61
61
  @connection.rollback
62
62
  end
63
63
 
64
+ # Create SAVEPOINT with specified name. Later use +rollback_to+ method to roll changes back
65
+ # to specified savepoint.
66
+ # Use beforehand
67
+ #
68
+ # plsql.connection.autocommit = false
69
+ #
70
+ # to turn off automatic commits after each statement.
71
+ def savepoint(name)
72
+ execute "SAVEPOINT #{name}"
73
+ end
74
+
75
+ # Roll back changes to specified savepoint (that was created using +savepoint+ method)
76
+ # Use beforehand
77
+ #
78
+ # plsql.connection.autocommit = false
79
+ #
80
+ # to turn off automatic commits after each statement.
81
+ def rollback_to(name)
82
+ execute "ROLLBACK TO #{name}"
83
+ end
84
+
64
85
  end
65
86
  end
66
87
 
data/lib/plsql/table.rb CHANGED
@@ -49,7 +49,8 @@ module PLSQL
49
49
  CASE WHEN c.data_type_owner IS NULL THEN NULL
50
50
  ELSE (SELECT t.typecode FROM all_types t
51
51
  WHERE t.owner = c.data_type_owner
52
- AND t.type_name = c.data_type) END typecode
52
+ AND t.type_name = c.data_type) END typecode,
53
+ c.nullable, c.data_default
53
54
  FROM all_tab_columns c
54
55
  WHERE c.owner = :owner
55
56
  AND c.table_name = :table_name",
@@ -57,7 +58,10 @@ module PLSQL
57
58
  ) do |r|
58
59
  column_name, position,
59
60
  data_type, data_length, data_precision, data_scale, char_used,
60
- data_type_owner, data_type_mod, typecode = r
61
+ data_type_owner, data_type_mod, typecode, nullable, data_default = r
62
+ # remove scale (n) from data_type (returned for TIMESTAMPs and INTERVALs)
63
+ data_type.sub!(/\(\d+\)/,'')
64
+ # store column metadata
61
65
  @columns[column_name.downcase.to_sym] = {
62
66
  :position => position && position.to_i,
63
67
  :data_type => data_type_owner && (typecode == 'COLLECTION' ? 'TABLE' : 'OBJECT' ) || data_type,
@@ -67,7 +71,9 @@ module PLSQL
67
71
  :char_used => char_used,
68
72
  :type_owner => data_type_owner,
69
73
  :type_name => data_type_owner && data_type,
70
- :sql_type_name => data_type_owner && "#{data_type_owner}.#{data_type}"
74
+ :sql_type_name => data_type_owner && "#{data_type_owner}.#{data_type}",
75
+ :nullable => nullable == 'Y', # store as true or false
76
+ :data_default => data_default && data_default.strip # remove leading and trailing whitespace
71
77
  }
72
78
  end
73
79
  end
@@ -98,7 +104,7 @@ module PLSQL
98
104
  order_by_sql = nil
99
105
  sql_params.each do |k,v|
100
106
  if k == :order_by
101
- order_by_sql = "ORDER BY #{v} "
107
+ order_by_sql = " ORDER BY #{v} "
102
108
  elsif v.nil?
103
109
  where_sqls << "#{k} IS NULL"
104
110
  else
data/lib/plsql/type.rb CHANGED
@@ -34,19 +34,23 @@ module PLSQL
34
34
  class Type
35
35
  extend TypeClassMethods
36
36
 
37
- attr_reader :typecode, :attributes, :schema_name, :type_name #:nodoc:
37
+ attr_reader :typecode, :attributes, :schema_name, :type_name, :type_object_id #:nodoc:
38
38
 
39
39
  def initialize(schema, type, override_schema_name = nil) #:nodoc:
40
40
  @schema = schema
41
41
  @schema_name = override_schema_name || schema.schema_name
42
42
  @type_name = type.to_s.upcase
43
43
  @attributes = {}
44
+ @type_procedures = {}
44
45
 
45
- @typecode = @schema.select_first(
46
- "SELECT typecode FROM all_types
47
- WHERE owner = :owner
48
- AND type_name = :type_name",
49
- @schema_name, @type_name)[0]
46
+ @typecode, @type_object_id = @schema.select_first(
47
+ "SELECT t.typecode, o.object_id FROM all_types t, all_objects o
48
+ WHERE t.owner = :owner
49
+ AND t.type_name = :type_name
50
+ AND o.owner = t.owner
51
+ AND o.object_name = t.type_name
52
+ AND o.object_type = 'TYPE'",
53
+ @schema_name, @type_name)
50
54
 
51
55
  @schema.select_all(
52
56
  "SELECT attr_name, attr_no,
@@ -77,11 +81,195 @@ module PLSQL
77
81
  end
78
82
  end
79
83
 
84
+ # is type collection?
85
+ def collection?
86
+ @is_collection ||= @typecode == 'COLLECTION'
87
+ end
88
+
80
89
  # list of object type attribute names
81
90
  def attribute_names
82
91
  @attribute_names ||= @attributes.keys.sort_by{|k| @attributes[k][:position]}
83
92
  end
84
93
 
94
+ # create new PL/SQL object instance
95
+ def new(*args, &block)
96
+ procedure = find_procedure(:new)
97
+ # in case of collections pass array of elements as one argument for constructor
98
+ if collection? && !(args.size == 1 && args[0].is_a?(Array))
99
+ args = [args]
100
+ end
101
+ result = procedure.exec_with_options(args, {:skip_self => true}, &block)
102
+ # TODO: collection constructor should return Array of ObhjectInstance objects
103
+ if collection?
104
+ result
105
+ else
106
+ # TODO: what to do if block is passed to constructor?
107
+ ObjectInstance.create(self, result)
108
+ end
109
+ end
110
+
111
+ def method_missing(method, *args, &block) #:nodoc:
112
+ if procedure = find_procedure(method)
113
+ procedure.exec_with_options(args, {}, &block)
114
+ else
115
+ raise ArgumentError, "No PL/SQL procedure '#{method.to_s.upcase}' found for type '#{@type_name}'"
116
+ end
117
+ end
118
+
119
+ def find_procedure(new_or_procedure) #:nodoc:
120
+ @type_procedures[new_or_procedure] ||= begin
121
+ procedure_name = new_or_procedure == :new ? @type_name : new_or_procedure
122
+ # find defined procedure for type
123
+ if @schema.select_first(
124
+ "SELECT procedure_name FROM all_procedures
125
+ WHERE owner = :owner
126
+ AND object_name = :object_name
127
+ AND procedure_name = :procedure_name",
128
+ @schema_name, @type_name, procedure_name.to_s.upcase)
129
+ TypeProcedure.new(@schema, self, procedure_name)
130
+ # call default constructor
131
+ elsif new_or_procedure == :new
132
+ TypeProcedure.new(@schema, self, :new)
133
+ end
134
+ end
135
+ end
136
+
137
+ # wrapper class to simulate Procedure class for ProcedureClass#exec
138
+ class TypeProcedure #:nodoc:
139
+ include ProcedureCommon
140
+
141
+ def initialize(schema, type, procedure)
142
+ @schema = schema
143
+ @type = type
144
+ @schema_name = @type.schema_name
145
+ @type_name = @type.type_name
146
+ @object_id = @type.type_object_id
147
+
148
+ # if default constructor
149
+ if @default_constructor = (procedure == :new)
150
+ @procedure = @type.collection? ? nil : @type_name
151
+ set_default_constructor_arguments
152
+ # if defined type procedure
153
+ else
154
+ @procedure = procedure.to_s.upcase
155
+ get_argument_metadata
156
+ # add also definition for default constructor in case of custom constructor
157
+ set_default_constructor_arguments if @procedure == @type_name
158
+ end
159
+
160
+ # constructors do not need type prefix in call
161
+ @package = @procedure == @type_name ? nil : @type_name
162
+ end
163
+
164
+ # will be called for collection constructor
165
+ def call_sql(params_string)
166
+ "#{params_string};\n"
167
+ end
168
+
169
+ attr_reader :arguments, :argument_list, :out_list
170
+ def arguments_without_self
171
+ @arguments_without_self ||= begin
172
+ hash = {}
173
+ @arguments.each do |ov, args|
174
+ hash[ov] = args.reject{|key, value| key == :self}
175
+ end
176
+ hash
177
+ end
178
+ end
179
+
180
+ def argument_list_without_self
181
+ @argument_list_without_self ||= begin
182
+ hash = {}
183
+ @argument_list.each do |ov, arg_list|
184
+ hash[ov] = arg_list.select{|arg| arg != :self}
185
+ end
186
+ hash
187
+ end
188
+ end
189
+
190
+ def out_list_without_self
191
+ @out_list_without_self ||= begin
192
+ hash = {}
193
+ @out_list.each do |ov, out_list|
194
+ hash[ov] = out_list.select{|arg| arg != :self}
195
+ end
196
+ hash
197
+ end
198
+ end
199
+
200
+ def exec_with_options(args, options={}, &block)
201
+ call = ProcedureCall.new(self, args, options)
202
+ result = call.exec(&block)
203
+ # if procedure was called then modified object is returned in SELF output parameter
204
+ if result.is_a?(Hash) && result[:self]
205
+ object = result.delete(:self)
206
+ result.empty? ? object : [object, result]
207
+ else
208
+ result
209
+ end
210
+ end
211
+
212
+ private
213
+
214
+ def set_default_constructor_arguments
215
+ @arguments ||= {}
216
+ @argument_list ||= {}
217
+ @out_list ||= {}
218
+ @return ||= {}
219
+ # either this will be the only overload or it will be additional
220
+ overload = @arguments.keys.size
221
+ # if type is collection then expect array of objects as argument
222
+ if @type.collection?
223
+ @arguments[overload] = {
224
+ :value => {
225
+ :position => 1,
226
+ :data_type => 'TABLE',
227
+ :in_out => 'IN',
228
+ :type_owner => @schema_name,
229
+ :type_name => @type_name,
230
+ :sql_type_name => "#{@schema_name}.#{@type_name}"
231
+ }
232
+ }
233
+ # otherwise if type is object type then expect object attributes as argument list
234
+ else
235
+ @arguments[overload] = @type.attributes
236
+ end
237
+ attributes = @arguments[overload]
238
+ @argument_list[overload] = attributes.keys.sort {|k1, k2| attributes[k1][:position] <=> attributes[k2][:position]}
239
+ # returns object or collection
240
+ @return[overload] = {
241
+ :position => 0,
242
+ :data_type => @type.collection? ? 'TABLE' : 'OBJECT',
243
+ :in_out => 'OUT',
244
+ :type_owner => @schema_name,
245
+ :type_name => @type_name,
246
+ :sql_type_name => "#{@schema_name}.#{@type_name}"
247
+ }
248
+ @out_list[overload] = []
249
+ @overloaded = overload > 0
250
+ end
251
+
252
+ end
253
+
254
+ end
255
+
256
+ class ObjectInstance < Hash #:nodoc:
257
+ attr_accessor :plsql_type
258
+
259
+ def self.create(type, attributes)
260
+ object = self.new.merge!(attributes)
261
+ object.plsql_type = type
262
+ object
263
+ end
264
+
265
+ def method_missing(method, *args, &block)
266
+ if procedure = @plsql_type.find_procedure(method)
267
+ procedure.exec_with_options(args, :self => self, &block)
268
+ else
269
+ raise ArgumentError, "No PL/SQL procedure '#{method.to_s.upcase}' found for type '#{@plsql_type.type_name}' object"
270
+ end
271
+ end
272
+
85
273
  end
86
274
 
87
275
  end
@@ -10,7 +10,7 @@ module PLSQL
10
10
  AND type = 'PACKAGE'
11
11
  AND UPPER(text) LIKE :variable_name",
12
12
  override_schema_name || schema.schema_name, package, "%#{variable_upcase}%").each do |row|
13
- if row[0] =~ /^\s*#{variable_upcase}\s+(CONSTANT\s+)?([A-Z0-9_. %]+(\([0-9,]+\))?)\s*(NOT\s+NULL)?\s*((:=|DEFAULT).*)?;\s*(--.*)?$/i
13
+ if row[0] =~ /^\s*#{variable_upcase}\s+(CONSTANT\s+)?([A-Z0-9_. %]+(\([\w\s,]+\))?)\s*(NOT\s+NULL)?\s*((:=|DEFAULT).*)?;\s*(--.*)?$/i
14
14
  return new(schema, variable, package, $2.strip, override_schema_name)
15
15
  end
16
16
  end
@@ -47,7 +47,7 @@ module PLSQL
47
47
 
48
48
  def metadata(type_string)
49
49
  case type_string
50
- when /^(VARCHAR2|CHAR|NVARCHAR2|NCHAR)(\((\d+)\))?$/
50
+ when /^(VARCHAR2|CHAR|NVARCHAR2|NCHAR)(\((\d+)[\s\w]*\))?$/
51
51
  {:data_type => $1, :data_length => $3.to_i, :in_out => 'IN/OUT'}
52
52
  when /^(CLOB|NCLOB|BLOB)$/,
53
53
  /^(NUMBER)(\(.*\))?$/, /^(PLS_INTEGER|BINARY_INTEGER)$/,
@@ -63,7 +63,7 @@ module PLSQL
63
63
  column = table.columns[$3.downcase.to_sym]
64
64
  {:data_type => column[:data_type], :data_length => column[:data_length], :sql_type_name => column[:sql_type_name], :in_out => 'IN/OUT'}
65
65
  when /^(\w+\.)?(\w+)$/
66
- schema = $1 ? plsql.send($1.chop) : plsql
66
+ schema = $1 ? @schema.root_schema.send($1.chop) : @schema
67
67
  begin
68
68
  type = schema.send($2.downcase.to_sym)
69
69
  raise ArgumentError unless type.is_a?(PLSQL::Type)
@@ -404,11 +404,86 @@ describe "Connection" do
404
404
 
405
405
  end
406
406
 
407
- describe "database version" do
407
+ describe "session information" do
408
408
  it "should get database version" do
409
409
  # using Oracle version 10.2.0.4 for unit tests
410
410
  @conn.database_version.should == [10, 2]
411
411
  end
412
+
413
+ it "should get session ID" do
414
+ @conn.session_id.should == @conn.select_first("SELECT USERENV('SESSIONID') FROM dual")[0].to_i
415
+ end
416
+ end
417
+
418
+ describe "drop ruby temporary tables" do
419
+ after(:all) do
420
+ @conn.drop_all_ruby_temporary_tables
421
+ end
422
+
423
+ it "should drop all ruby temporary tables" do
424
+ tmp_table = "ruby_111_222_333"
425
+ @conn.exec "CREATE GLOBAL TEMPORARY TABLE #{tmp_table} (dummy CHAR(1))"
426
+ lambda { @conn.select_first("SELECT * FROM #{tmp_table}") }.should_not raise_error
427
+ @conn.drop_all_ruby_temporary_tables
428
+ lambda { @conn.select_first("SELECT * FROM #{tmp_table}") }.should raise_error(/table or view does not exist/)
429
+ end
430
+
431
+ it "should drop current session ruby temporary tables" do
432
+ tmp_table = "ruby_#{@conn.session_id}_222_333"
433
+ @conn.exec "CREATE GLOBAL TEMPORARY TABLE #{tmp_table} (dummy CHAR(1))"
434
+ lambda { @conn.select_first("SELECT * FROM #{tmp_table}") }.should_not raise_error
435
+ @conn.drop_session_ruby_temporary_tables
436
+ lambda { @conn.select_first("SELECT * FROM #{tmp_table}") }.should raise_error(/table or view does not exist/)
437
+ end
438
+
439
+ it "should not drop other session ruby temporary tables" do
440
+ tmp_table = "ruby_#{@conn.session_id+1}_222_333"
441
+ @conn.exec "CREATE GLOBAL TEMPORARY TABLE #{tmp_table} (dummy CHAR(1))"
442
+ lambda { @conn.select_first("SELECT * FROM #{tmp_table}") }.should_not raise_error
443
+ @conn.drop_session_ruby_temporary_tables
444
+ lambda { @conn.select_first("SELECT * FROM #{tmp_table}") }.should_not raise_error
445
+ end
446
+
447
+ end
448
+
449
+ describe "logoff" do
450
+ before(:each) do
451
+ # restore connection before each test
452
+ reconnect_connection
453
+ end
454
+
455
+ after(:all) do
456
+ @conn.exec "DROP TABLE test_dummy_table" rescue nil
457
+ end
458
+
459
+ def reconnect_connection
460
+ @raw_conn = get_connection
461
+ @conn = PLSQL::Connection.create( @raw_conn )
462
+ end
463
+
464
+ it "should drop current session ruby temporary tables" do
465
+ tmp_table = "ruby_#{@conn.session_id}_222_333"
466
+ @conn.exec "CREATE GLOBAL TEMPORARY TABLE #{tmp_table} (dummy CHAR(1))"
467
+ lambda { @conn.select_first("SELECT * FROM #{tmp_table}") }.should_not raise_error
468
+ @conn.logoff
469
+ reconnect_connection
470
+ lambda { @conn.select_first("SELECT * FROM #{tmp_table}") }.should raise_error(/table or view does not exist/)
471
+ end
472
+
473
+ it "should rollback any uncommited transactions" do
474
+ tmp_table = "ruby_#{@conn.session_id}_222_333"
475
+ old_autocommit = @conn.autocommit?
476
+ @conn.autocommit = false
477
+ @conn.exec "CREATE GLOBAL TEMPORARY TABLE #{tmp_table} (dummy CHAR(1))"
478
+ @conn.exec "CREATE TABLE test_dummy_table (dummy CHAR(1))"
479
+ @conn.exec "INSERT INTO test_dummy_table VALUES ('1')"
480
+ # logoff will drop ruby temporary tables, it should do rollback before drop table
481
+ @conn.logoff
482
+ reconnect_connection
483
+ @conn.select_first("SELECT * FROM test_dummy_table").should == nil
484
+ @conn.autocommit = old_autocommit
485
+ end
486
+
412
487
  end
413
488
 
414
489
  end