ruby-plsql 0.4.1 → 0.4.2

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