ruby-plsql 0.4.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,3 +1,22 @@
1
+ == 0.4.1 2010-01-04
2
+
3
+ * New features
4
+ * Call procedures from SYS.STANDARD without schema and package prefix
5
+ * DBMS_OUTPUT logging to specified IO stream (e.g. plsql.dbms_output_stream = STDOUT)
6
+ * Support table operations also on views
7
+ * Specify plsql.connection.prefetch_rows= to reduce network round trips when selecting large number of rows
8
+ * Support for PLS_INTEGER and BINARY_INTEGER parameters and return values
9
+ * Access to package variables (basic types, object types, %TYPE and %ROWTYPE)
10
+ * Table insert_values method
11
+ * Insert partial list of table column values (and use default values for missing columns)
12
+ * Improvements
13
+ * Improved performance of table and synonyms metadata select
14
+ * Check required ruby-oci8 version
15
+ * Bug fixes
16
+ * limit object types when selecting from all_objects to avoid getting irrelevant records with the same name
17
+ * select where condition :column => nil is transformed to "column IS NULL"
18
+ * TIMESTAMP fractional seconds patch for ruby-oci8 2.0.3
19
+
1
20
  == 0.4.0 2009-11-23
2
21
 
3
22
  * New features
data/License.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2009 Raimonds Simanovskis
1
+ Copyright (c) 2008-2010 Raimonds Simanovskis
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.rdoc CHANGED
@@ -6,9 +6,9 @@ Ruby API for calling Oracle PL/SQL procedures.
6
6
 
7
7
  ruby-plsql gem provides simple Ruby API for calling Oracle PL/SQL procedures. It could be used both for accessing Oracle PL/SQL API procedures in legacy applications as well as it could be used to create PL/SQL unit tests using Ruby testing libraries.
8
8
 
9
- NUMBER, VARCHAR2, DATE, TIMESTAMP, CLOB, BLOB, BOOLEAN, PL/SQL RECORD, TABLE, VARRAY, OBJECT and CURSOR types are supported for input and output parameters and return values of PL/SQL procedures and functions.
9
+ NUMBER, BINARY_INTEGER, PLS_INTEGER, VARCHAR2, NVARCHAR2, CHAR, NCHAR, DATE, TIMESTAMP, CLOB, BLOB, BOOLEAN, PL/SQL RECORD, TABLE, VARRAY, OBJECT and CURSOR types are supported for input and output parameters and return values of PL/SQL procedures and functions.
10
10
 
11
- ruby-plsql support both Ruby 1.8 MRI, Ruby 1.9.1 YARV and JRuby 1.3/1.4 runtime environments.
11
+ ruby-plsql supports both Ruby 1.8 MRI, Ruby 1.9.1 YARV and JRuby 1.3/1.4 runtime environments.
12
12
 
13
13
  == USAGE
14
14
 
@@ -36,7 +36,7 @@ ruby-plsql support both Ruby 1.8 MRI, Ruby 1.9.1 YARV and JRuby 1.3/1.4 runtime
36
36
 
37
37
  # Nested objects or arrays are also supported
38
38
  p_employee = { :employee_id => 1, :first_name => 'First', :last_name => 'Last', :hire_date => Time.local(2000,01,31),
39
- :address => {:street => 'Street', :city => 'City', :country => 'Country},
39
+ :address => {:street => 'Street', :city => 'City', :country => 'Country'},
40
40
  :phones => [{:type => 'mobile', :phone_number => '123456'}, {:type => 'fixed', :phone_number => '654321'}]}
41
41
  plsql.test_store_employee(p_employee)
42
42
 
@@ -67,6 +67,11 @@ ruby-plsql also provides simple API for select/insert/update/delete table operat
67
67
  employees = [employee1, employee2, ... ] # array of many Hashes
68
68
  plsql.employees.insert employees
69
69
 
70
+ # insert many records as list of values
71
+ plsql.employees.insert_values [:employee_id, :first_name, :last_name],
72
+ [1, 'First 1', 'Last 1'],
73
+ [2, 'First 2', 'Last 2']
74
+
70
75
  # select one record
71
76
  plsql.employees.first # SELECT * FROM employees
72
77
  # fetch first row => {:employee_id => ..., :first_name => '...', ...}
@@ -131,7 +136,7 @@ In addition install either ruby-oci8 (for MRI/YARV) or copy Oracle JDBC driver t
131
136
 
132
137
  (The MIT License)
133
138
 
134
- Copyright (c) 2009 Raimonds Simanovskis
139
+ Copyright (c) 2008-2010 Raimonds Simanovskis
135
140
 
136
141
  Permission is hereby granted, free of charge, to any person obtaining
137
142
  a copy of this software and associated documentation files (the
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.0
1
+ 0.4.1
@@ -65,22 +65,28 @@ module PLSQL
65
65
  raise NoMethodError, "Not implemented for this raw driver"
66
66
  end
67
67
 
68
+ # Set number of rows to be prefetched. This can reduce the number of network round trips when fetching many rows.
69
+ # The default value is one. (If ActiveRecord oracle_enhanced connection is used then default is 100)
70
+ def prefetch_rows=(value)
71
+ raise NoMethodError, "Not implemented for this raw driver"
72
+ end
73
+
68
74
  def select_first(sql, *bindvars) #:nodoc:
69
- cursor = cursor_from_query(self, sql, *bindvars)
75
+ cursor = cursor_from_query(sql, bindvars, :prefetch_rows => 1)
70
76
  cursor.fetch
71
77
  ensure
72
78
  cursor.close rescue nil
73
79
  end
74
80
 
75
81
  def select_hash_first(sql, *bindvars) #:nodoc:
76
- cursor = cursor_from_query(self, sql, *bindvars)
82
+ cursor = cursor_from_query(sql, bindvars, :prefetch_rows => 1)
77
83
  cursor.fetch_hash
78
84
  ensure
79
85
  cursor.close rescue nil
80
86
  end
81
87
 
82
88
  def select_all(sql, *bindvars, &block) #:nodoc:
83
- cursor = cursor_from_query(self, sql, *bindvars)
89
+ cursor = cursor_from_query(sql, bindvars)
84
90
  results = []
85
91
  row_count = 0
86
92
  while row = cursor.fetch
@@ -97,7 +103,7 @@ module PLSQL
97
103
  end
98
104
 
99
105
  def select_hash_all(sql, *bindvars, &block) #:nodoc:
100
- cursor = cursor_from_query(self, sql, *bindvars)
106
+ cursor = cursor_from_query(sql, bindvars)
101
107
  results = []
102
108
  row_count = 0
103
109
  while row = cursor.fetch_hash
@@ -121,10 +127,6 @@ module PLSQL
121
127
  raise NoMethodError, "Not implemented for this raw driver"
122
128
  end
123
129
 
124
- def arrays_to_hash(keys, values) #:nodoc:
125
- (0...keys.size).inject({}) { |hash, i| hash[keys[i]] = values[i]; hash }
126
- end
127
-
128
130
  module CursorCommon
129
131
  # Fetch all rows from cursor, each row as array of values
130
132
  def fetch_all
@@ -146,10 +148,23 @@ module PLSQL
146
148
 
147
149
  # Fetch row from cursor as hash {:column => value, ...}
148
150
  def fetch_hash
149
- (row = fetch) && @connection.arrays_to_hash(fields, row)
151
+ (row = fetch) && ArrayHelpers::to_hash(fields, row)
150
152
  end
151
153
  end
152
154
 
155
+ # all_synonyms view is quite slow therefore
156
+ # this implementation is overriden in OCI connection with faster native OCI method
157
+ def describe_synonym(schema_name, synonym_name) #:nodoc:
158
+ select_first(
159
+ "SELECT table_owner, table_name FROM all_synonyms WHERE owner = :owner AND synonym_name = :synonym_name",
160
+ schema_name.to_s.upcase, synonym_name.to_s.upcase)
161
+ end
162
+
163
+ # Returns array with major and minor version of database (e.g. [10, 2])
164
+ def database_version
165
+ raise NoMethodError, "Not implemented for this raw driver"
166
+ end
167
+
153
168
  end
154
169
 
155
170
  end
@@ -0,0 +1,9 @@
1
+ module PLSQL #:nodoc:
2
+ module ArrayHelpers #:nodoc:
3
+
4
+ def self.to_hash(keys, values) #:nodoc:
5
+ (0...keys.size).inject({}) { |hash, i| hash[keys[i]] = values[i]; hash }
6
+ end
7
+
8
+ end
9
+ end
@@ -54,6 +54,10 @@ module PLSQL
54
54
  raw_connection.setAutoCommit(value)
55
55
  end
56
56
 
57
+ def prefetch_rows=(value)
58
+ raw_connection.setDefaultRowPrefetch(value)
59
+ end
60
+
57
61
  def exec(sql, *bindvars)
58
62
  cs = prepare_call(sql, *bindvars)
59
63
  cs.execute
@@ -127,8 +131,11 @@ module PLSQL
127
131
  end
128
132
  end
129
133
 
130
- def self.new_from_query(conn, sql, *bindvars)
134
+ def self.new_from_query(conn, sql, bindvars=[], options={})
131
135
  stmt = conn.prepare_statement(sql, *bindvars)
136
+ if prefetch_rows = options[:prefetch_rows]
137
+ stmt.setRowPrefetch(prefetch_rows)
138
+ end
132
139
  cursor = Cursor.new(conn, stmt.executeQuery)
133
140
  cursor.statement = stmt
134
141
  cursor
@@ -164,8 +171,8 @@ module PLSQL
164
171
  CallableStatement.new(self, sql)
165
172
  end
166
173
 
167
- def cursor_from_query(sql, *bindvars)
168
- Cursor.new_from_query(sql, *bindvars)
174
+ def cursor_from_query(sql, bindvars=[], options={})
175
+ Cursor.new_from_query(self, sql, bindvars, options)
169
176
  end
170
177
 
171
178
  def prepare_statement(sql, *bindvars)
@@ -194,7 +201,7 @@ module PLSQL
194
201
  Java::OracleSql::CLOB => Java::oracle.jdbc.OracleTypes::CLOB,
195
202
  Java::OracleSql::BLOB => Java::oracle.jdbc.OracleTypes::BLOB,
196
203
  Date => java.sql.Types::DATE,
197
- Time => java.sql.Types::DATE,
204
+ Time => java.sql.Types::TIMESTAMP,
198
205
  DateTime => java.sql.Types::DATE,
199
206
  Java::OracleSql::ARRAY => Java::oracle.jdbc.OracleTypes::ARRAY,
200
207
  Array => Java::oracle.jdbc.OracleTypes::ARRAY,
@@ -207,6 +214,7 @@ module PLSQL
207
214
  java.sql.Types::CHAR => String,
208
215
  java.sql.Types::VARCHAR => String,
209
216
  java.sql.Types::NUMERIC => BigDecimal,
217
+ java.sql.Types::INTEGER => Fixnum,
210
218
  java.sql.Types::DATE => Time,
211
219
  java.sql.Types::TIMESTAMP => Time,
212
220
  Java::oracle.jdbc.OracleTypes::TIMESTAMPTZ => Time,
@@ -238,8 +246,10 @@ module PLSQL
238
246
  stmt.send("setClob#{key && "AtName"}", key || i, value)
239
247
  when :'Java::OracleSql::BLOB'
240
248
  stmt.send("setBlob#{key && "AtName"}", key || i, value)
241
- when :Date, :Time, :DateTime, :'Java::OracleSql::DATE'
249
+ when :Date, :DateTime, :'Java::OracleSql::DATE'
242
250
  stmt.send("setDATE#{key && "AtName"}", key || i, value)
251
+ when :Time, :'Java::JavaSql::Timestamp'
252
+ stmt.send("setTimestamp#{key && "AtName"}", key || i, value)
243
253
  when :NilClass
244
254
  if ['TABLE', 'VARRAY', 'OBJECT'].include?(metadata[:data_type])
245
255
  stmt.send("setNull#{key && "AtName"}", key || i, get_java_sql_type(value, type),
@@ -280,8 +290,10 @@ module PLSQL
280
290
  stmt.getClob(i)
281
291
  when :'Java::OracleSql::BLOB'
282
292
  stmt.getBlob(i)
283
- when :Date, :Time, :DateTime
293
+ when :Date, :DateTime
284
294
  stmt.getDATE(i)
295
+ when :Time
296
+ stmt.getTimestamp(i)
285
297
  when :'Java::OracleSql::ARRAY'
286
298
  stmt.getArray(i)
287
299
  when :'Java::OracleSql::STRUCT'
@@ -312,8 +324,10 @@ module PLSQL
312
324
  [Java::OracleSql::BLOB, nil]
313
325
  when "NUMBER"
314
326
  [BigDecimal, nil]
327
+ when "PLS_INTEGER", "BINARY_INTEGER"
328
+ [Fixnum, nil]
315
329
  when "DATE"
316
- [Time, nil]
330
+ [DateTime, nil]
317
331
  when "TIMESTAMP", "TIMESTAMP WITH TIME ZONE", "TIMESTAMP WITH LOCAL TIME ZONE"
318
332
  [Time, nil]
319
333
  when "TABLE", "VARRAY"
@@ -341,7 +355,7 @@ module PLSQL
341
355
  else
342
356
  java_bigdecimal(value)
343
357
  end
344
- when :Time, :Date, :DateTime
358
+ when :Date, :DateTime
345
359
  case value
346
360
  when DateTime
347
361
  java_date(Time.send(plsql.default_timezone, value.year, value.month, value.day, value.hour, value.min, value.sec))
@@ -350,6 +364,8 @@ module PLSQL
350
364
  else
351
365
  java_date(value)
352
366
  end
367
+ when :Time
368
+ java_timestamp(value)
353
369
  when :'Java::OracleSql::CLOB'
354
370
  if value
355
371
  clob = Java::OracleSql::CLOB.createTemporary(raw_connection, false, Java::OracleSql::CLOB::DURATION_SESSION)
@@ -431,6 +447,11 @@ module PLSQL
431
447
  t = value.timeValue
432
448
  Time.send(plsql.default_timezone, d.year + 1900, d.month + 1, d.date, t.hours, t.minutes, t.seconds)
433
449
  end
450
+ when Java::JavaSql::Timestamp
451
+ if value
452
+ Time.send(plsql.default_timezone, value.year + 1900, value.month + 1, value.date, value.hours, value.minutes, value.seconds,
453
+ value.nanos / 1000)
454
+ end
434
455
  when Java::OracleSql::CLOB
435
456
  if value.isEmptyLob
436
457
  nil
@@ -450,7 +471,7 @@ module PLSQL
450
471
  struct_metadata = descriptor.getMetaData
451
472
  field_names = (1..descriptor.getLength).map {|i| struct_metadata.getColumnName(i).downcase.to_sym}
452
473
  field_values = value.getAttributes.map{|e| ora_value_to_ruby_value(e)}
453
- arrays_to_hash(field_names, field_values)
474
+ ArrayHelpers::to_hash(field_names, field_values)
454
475
  when Java::java.sql.ResultSet
455
476
  Cursor.new(self, value)
456
477
  else
@@ -458,12 +479,20 @@ module PLSQL
458
479
  end
459
480
  end
460
481
 
482
+ def database_version
483
+ @database_version ||= (md = raw_connection.getMetaData) && [md.getDatabaseMajorVersion, md.getDatabaseMinorVersion]
484
+ end
485
+
461
486
  private
462
487
 
463
488
  def java_date(value)
464
489
  value && Java::oracle.sql.DATE.new(value.strftime("%Y-%m-%d %H:%M:%S"))
465
490
  end
466
491
 
492
+ def java_timestamp(value)
493
+ value && Java::java.sql.Timestamp.new(value.year-1900, value.month-1, value.day, value.hour, value.min, value.sec, value.usec * 1000)
494
+ end
495
+
467
496
  def java_bigdecimal(value)
468
497
  value && java.math.BigDecimal.new(value.to_s)
469
498
  end
@@ -0,0 +1,25 @@
1
+ # apply TIMESTAMP fractional seconds patch to ruby-oci8 2.0.3
2
+ # see http://rubyforge.org/forum/forum.php?thread_id=46576&forum_id=1078
3
+ if OCI8::VERSION == "2.0.3" &&
4
+ !OCI8::BindType::Util.method_defined?(:datetime_to_array_without_timestamp_patch)
5
+
6
+ OCI8::BindType::Util.module_eval do
7
+ alias :datetime_to_array_without_timestamp_patch :datetime_to_array
8
+ def datetime_to_array(val, full)
9
+ result = datetime_to_array_without_timestamp_patch(val, full)
10
+ if result && result[6] == 0
11
+ if val.respond_to? :nsec
12
+ fsec = val.nsec
13
+ elsif val.respond_to? :usec
14
+ fsec = val.usec * 1000
15
+ else
16
+ fsec = 0
17
+ end
18
+ result[6] = fsec
19
+ end
20
+ result
21
+ end
22
+ private :datetime_to_array_without_timestamp_patch, :datetime_to_array
23
+ end
24
+
25
+ end
@@ -2,10 +2,16 @@ begin
2
2
  require "oci8"
3
3
  rescue LoadError
4
4
  # OCI8 driver is unavailable.
5
- error_message = "ERROR: ruby-plsql could not load ruby-oci8 library. "+
6
- "Please install ruby-oci8 gem."
7
- STDERR.puts error_message
8
- raise LoadError
5
+ raise LoadError, "ERROR: ruby-plsql could not load ruby-oci8 library. Please install ruby-oci8 gem."
6
+ end
7
+
8
+ require "plsql/oci8_patches"
9
+
10
+ # check ruby-oci8 version
11
+ required_oci8_version = [2, 0, 3]
12
+ oci8_version_ints = OCI8::VERSION.scan(/\d+/).map{|s| s.to_i}
13
+ if (oci8_version_ints <=> required_oci8_version) < 0
14
+ raise LoadError, "ERROR: ruby-oci8 version #{OCI8::VERSION} is too old. Please install ruby-oci8 version #{required_oci8_version.join('.')} or later."
9
15
  end
10
16
 
11
17
  module PLSQL
@@ -31,6 +37,10 @@ module PLSQL
31
37
  raw_connection.autocommit = value
32
38
  end
33
39
 
40
+ def prefetch_rows=(value)
41
+ raw_connection.prefetch_rows = value
42
+ end
43
+
34
44
  def exec(sql, *bindvars)
35
45
  raw_connection.exec(sql, *bindvars)
36
46
  true
@@ -54,8 +64,17 @@ module PLSQL
54
64
  self.new(conn, raw_cursor)
55
65
  end
56
66
 
57
- def self.new_from_query(conn, sql, *bindvars)
58
- Cursor.new(conn, conn.raw_connection.exec(sql, *bindvars))
67
+ def self.new_from_query(conn, sql, bindvars=[], options={})
68
+ cursor = new_from_parse(conn, sql)
69
+ if prefetch_rows = options[:prefetch_rows]
70
+ cursor.prefetch_rows = prefetch_rows
71
+ end
72
+ cursor.exec(*bindvars)
73
+ cursor
74
+ end
75
+
76
+ def prefetch_rows=(value)
77
+ @raw_cursor.prefetch_rows = value
59
78
  end
60
79
 
61
80
  def bind_param(arg, value, metadata)
@@ -99,24 +118,24 @@ module PLSQL
99
118
  Cursor.new_from_parse(self, sql)
100
119
  end
101
120
 
102
- def cursor_from_query(sql, *bindvars)
103
- Cursor.new_from_query(sql, *bindvars)
121
+ def cursor_from_query(sql, bindvars=[], options={})
122
+ Cursor.new_from_query(self, sql, bindvars, options)
104
123
  end
105
124
 
106
125
  def plsql_to_ruby_data_type(metadata)
107
126
  data_type, data_length = metadata[:data_type], metadata[:data_length]
108
127
  case data_type
109
- when "VARCHAR2"
128
+ when "VARCHAR2", "CHAR", "NVARCHAR2", "NCHAR"
110
129
  [String, data_length || 32767]
111
- when "CLOB"
130
+ when "CLOB", "NCLOB"
112
131
  [OCI8::CLOB, nil]
113
132
  when "BLOB"
114
133
  [OCI8::BLOB, nil]
115
- when "NUMBER"
134
+ when "NUMBER", "PLS_INTEGER", "BINARY_INTEGER"
116
135
  [OraNumber, nil]
117
136
  when "DATE"
118
137
  [DateTime, nil]
119
- when "TIMESTAMP"
138
+ when "TIMESTAMP", "TIMESTAMP WITH TIME ZONE", "TIMESTAMP WITH LOCAL TIME ZONE"
120
139
  [Time, nil]
121
140
  when "TABLE", "VARRAY", "OBJECT"
122
141
  # create Ruby class for collection
@@ -240,6 +259,21 @@ module PLSQL
240
259
  end
241
260
  end
242
261
 
262
+ def describe_synonym(schema_name, synonym_name)
263
+ if schema_name == 'PUBLIC'
264
+ full_name = synonym_name.to_s
265
+ else
266
+ full_name = "#{schema_name}.#{synonym_name}"
267
+ end
268
+ metadata = raw_connection.describe_synonym(full_name)
269
+ [metadata.schema_name, metadata.name]
270
+ rescue OCIError
271
+ nil
272
+ end
273
+
274
+ def database_version
275
+ @database_version ||= (version = raw_connection.oracle_server_version) && [version.major, version.minor]
276
+ end
243
277
 
244
278
  private
245
279
 
data/lib/plsql/package.rb CHANGED
@@ -2,16 +2,16 @@ module PLSQL
2
2
 
3
3
  module PackageClassMethods #:nodoc:
4
4
  def find(schema, package)
5
- if schema.select_first("
6
- SELECT object_name FROM all_objects
5
+ if schema.select_first(
6
+ "SELECT object_name FROM all_objects
7
7
  WHERE owner = :owner
8
8
  AND object_name = :package
9
9
  AND object_type = 'PACKAGE'",
10
10
  schema.schema_name, package.to_s.upcase)
11
11
  new(schema, package)
12
12
  # search for synonym
13
- elsif (row = schema.select_first("
14
- SELECT o.owner, o.object_name
13
+ elsif (row = schema.select_first(
14
+ "SELECT o.owner, o.object_name
15
15
  FROM all_synonyms s, all_objects o
16
16
  WHERE s.owner IN (:owner, 'PUBLIC')
17
17
  AND s.synonym_name = :synonym_name
@@ -34,22 +34,35 @@ module PLSQL
34
34
  @schema = schema
35
35
  @override_schema_name = override_schema_name
36
36
  @package = package.to_s.upcase
37
- @procedures = {}
37
+ @package_objects = {}
38
38
  end
39
39
 
40
40
  private
41
41
 
42
42
  def method_missing(method, *args, &block)
43
- if procedure = @procedures[method]
44
- procedure.exec(*args, &block)
45
- elsif procedure = Procedure.find(@schema, method, @package, @override_schema_name)
46
- @procedures[method] = procedure
47
- procedure.exec(*args, &block)
43
+ if assignment = (method.to_s[-1,1] == '=')
44
+ method = method.to_s.chop.to_sym
45
+ end
46
+ object = (@package_objects[method] ||=
47
+ Procedure.find(@schema, method, @package, @override_schema_name) ||
48
+ Variable.find(@schema, method, @package, @override_schema_name))
49
+ case object
50
+ when Procedure
51
+ raise ArgumentError, "Cannot assign value to package procedure '#{method.to_s.upcase}'" if assignment
52
+ object.exec(*args, &block)
53
+ when Variable
54
+ if assignment
55
+ raise ArgumentError, "Just one value can be assigned to package variable '#{method.to_s.upcase}'" unless args.size == 1 && block == nil
56
+ object.value = args[0]
57
+ else
58
+ raise ArgumentError, "Cannot pass arguments when getting package variable '#{method.to_s.upcase}' value" unless args.size == 0 && block == nil
59
+ object.value
60
+ end
48
61
  else
49
- raise ArgumentError, "No PL/SQL procedure found"
62
+ raise ArgumentError, "No PL/SQL procedure or variable '#{method.to_s.upcase}' found"
50
63
  end
51
64
  end
52
-
65
+
53
66
  end
54
67
 
55
68
  end