ruby-plsql 0.4.0 → 0.4.1

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/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