pmacs-activerecord-oracle_enhanced-adapter 1.4.2.rc1

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.
Files changed (46) hide show
  1. data/.rspec +2 -0
  2. data/Gemfile +52 -0
  3. data/History.md +284 -0
  4. data/License.txt +20 -0
  5. data/README.md +403 -0
  6. data/RUNNING_TESTS.md +45 -0
  7. data/Rakefile +59 -0
  8. data/VERSION +1 -0
  9. data/lib/active_record/connection_adapters/emulation/oracle_adapter.rb +5 -0
  10. data/lib/active_record/connection_adapters/oracle_enhanced.rake +105 -0
  11. data/lib/active_record/connection_adapters/oracle_enhanced_activerecord_patches.rb +41 -0
  12. data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +1408 -0
  13. data/lib/active_record/connection_adapters/oracle_enhanced_base_ext.rb +118 -0
  14. data/lib/active_record/connection_adapters/oracle_enhanced_column.rb +141 -0
  15. data/lib/active_record/connection_adapters/oracle_enhanced_connection.rb +135 -0
  16. data/lib/active_record/connection_adapters/oracle_enhanced_context_index.rb +359 -0
  17. data/lib/active_record/connection_adapters/oracle_enhanced_core_ext.rb +25 -0
  18. data/lib/active_record/connection_adapters/oracle_enhanced_cpk.rb +21 -0
  19. data/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb +44 -0
  20. data/lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb +565 -0
  21. data/lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb +491 -0
  22. data/lib/active_record/connection_adapters/oracle_enhanced_procedures.rb +260 -0
  23. data/lib/active_record/connection_adapters/oracle_enhanced_schema_definitions.rb +231 -0
  24. data/lib/active_record/connection_adapters/oracle_enhanced_schema_dumper.rb +257 -0
  25. data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements.rb +397 -0
  26. data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements_ext.rb +265 -0
  27. data/lib/active_record/connection_adapters/oracle_enhanced_structure_dump.rb +294 -0
  28. data/lib/active_record/connection_adapters/oracle_enhanced_tasks.rb +17 -0
  29. data/lib/active_record/connection_adapters/oracle_enhanced_version.rb +1 -0
  30. data/lib/pmacs-activerecord-oracle_enhanced-adapter.rb +25 -0
  31. data/pmacs-activerecord-oracle_enhanced-adapter.gemspec +131 -0
  32. data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +778 -0
  33. data/spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb +332 -0
  34. data/spec/active_record/connection_adapters/oracle_enhanced_context_index_spec.rb +427 -0
  35. data/spec/active_record/connection_adapters/oracle_enhanced_core_ext_spec.rb +19 -0
  36. data/spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb +113 -0
  37. data/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +1376 -0
  38. data/spec/active_record/connection_adapters/oracle_enhanced_dbms_output_spec.rb +69 -0
  39. data/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb +141 -0
  40. data/spec/active_record/connection_adapters/oracle_enhanced_emulate_oracle_adapter_spec.rb +25 -0
  41. data/spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb +378 -0
  42. data/spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb +438 -0
  43. data/spec/active_record/connection_adapters/oracle_enhanced_schema_statements_spec.rb +1280 -0
  44. data/spec/active_record/connection_adapters/oracle_enhanced_structure_dump_spec.rb +339 -0
  45. data/spec/spec_helper.rb +187 -0
  46. metadata +302 -0
@@ -0,0 +1,491 @@
1
+ require 'delegate'
2
+
3
+ begin
4
+ require "oci8"
5
+ rescue LoadError
6
+ # OCI8 driver is unavailable.
7
+ raise LoadError, "ERROR: ActiveRecord oracle_enhanced adapter could not load ruby-oci8 library. Please install ruby-oci8 gem."
8
+ end
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."
15
+ end
16
+
17
+ module ActiveRecord
18
+ module ConnectionAdapters
19
+
20
+ # OCI database interface for MRI
21
+ class OracleEnhancedOCIConnection < OracleEnhancedConnection #:nodoc:
22
+
23
+ def initialize(config)
24
+ @raw_connection = OCI8EnhancedAutoRecover.new(config, OracleEnhancedOCIFactory)
25
+ # default schema owner
26
+ @owner = config[:username].to_s.upcase
27
+ end
28
+
29
+ def raw_oci_connection
30
+ if @raw_connection.is_a? OCI8
31
+ @raw_connection
32
+ # ActiveRecord Oracle enhanced adapter puts OCI8EnhancedAutoRecover wrapper around OCI8
33
+ # in this case we need to pass original OCI8 connection
34
+ else
35
+ @raw_connection.instance_variable_get(:@connection)
36
+ end
37
+ end
38
+
39
+ def auto_retry
40
+ @raw_connection.auto_retry if @raw_connection
41
+ end
42
+
43
+ def auto_retry=(value)
44
+ @raw_connection.auto_retry = value if @raw_connection
45
+ end
46
+
47
+ def logoff
48
+ @raw_connection.logoff
49
+ @raw_connection.active = false
50
+ end
51
+
52
+ def commit
53
+ @raw_connection.commit
54
+ end
55
+
56
+ def rollback
57
+ @raw_connection.rollback
58
+ end
59
+
60
+ def autocommit?
61
+ @raw_connection.autocommit?
62
+ end
63
+
64
+ def autocommit=(value)
65
+ @raw_connection.autocommit = value
66
+ end
67
+
68
+ # Checks connection, returns true if active. Note that ping actively
69
+ # checks the connection, while #active? simply returns the last
70
+ # known state.
71
+ def ping
72
+ @raw_connection.ping
73
+ rescue OCIException => e
74
+ raise OracleEnhancedConnectionException, e.message
75
+ end
76
+
77
+ def active?
78
+ @raw_connection.active?
79
+ end
80
+
81
+ def reset!
82
+ @raw_connection.reset!
83
+ rescue OCIException => e
84
+ raise OracleEnhancedConnectionException, e.message
85
+ end
86
+
87
+ def exec(sql, *bindvars, &block)
88
+ @raw_connection.exec(sql, *bindvars, &block)
89
+ end
90
+
91
+ def returning_clause(quoted_pk)
92
+ " RETURNING #{quoted_pk} INTO :insert_id"
93
+ end
94
+
95
+ # execute sql with RETURNING ... INTO :insert_id
96
+ # and return :insert_id value
97
+ def exec_with_returning(sql)
98
+ cursor = @raw_connection.parse(sql)
99
+ cursor.bind_param(':insert_id', nil, Integer)
100
+ cursor.exec
101
+ cursor[':insert_id']
102
+ ensure
103
+ cursor.close rescue nil
104
+ end
105
+
106
+ def prepare(sql)
107
+ Cursor.new(self, @raw_connection.parse(sql))
108
+ end
109
+
110
+ class Cursor
111
+ def initialize(connection, raw_cursor)
112
+ @connection = connection
113
+ @raw_cursor = raw_cursor
114
+ end
115
+
116
+ def bind_param(position, value, col_type = nil)
117
+ if value.nil?
118
+ @raw_cursor.bind_param(position, nil, String)
119
+ else
120
+ case col_type
121
+ when :text, :binary
122
+ # ruby-oci8 cannot create CLOB/BLOB from ''
123
+ lob_value = value == '' ? ' ' : value
124
+ bind_type = col_type == :text ? OCI8::CLOB : OCI8::BLOB
125
+ ora_value = bind_type.new(@connection.raw_oci_connection, lob_value)
126
+ ora_value.size = 0 if value == ''
127
+ @raw_cursor.bind_param(position, ora_value)
128
+ when :raw
129
+ @raw_cursor.bind_param(position, OracleEnhancedAdapter.encode_raw(value))
130
+ when :decimal
131
+ @raw_cursor.bind_param(position, BigDecimal.new(value.to_s))
132
+ else
133
+ @raw_cursor.bind_param(position, value)
134
+ end
135
+ end
136
+ end
137
+
138
+ def bind_returning_param(position, bind_type)
139
+ @raw_cursor.bind_param(position, nil, bind_type)
140
+ end
141
+
142
+ def exec
143
+ @raw_cursor.exec
144
+ end
145
+
146
+ def exec_update
147
+ @raw_cursor.exec
148
+ end
149
+
150
+ def get_col_names
151
+ @raw_cursor.get_col_names
152
+ end
153
+
154
+ def fetch(options={})
155
+ if row = @raw_cursor.fetch
156
+ get_lob_value = options[:get_lob_value]
157
+ row.map do |col|
158
+ @connection.typecast_result_value(col, get_lob_value)
159
+ end
160
+ end
161
+ end
162
+
163
+ def get_returning_param(position, type)
164
+ @raw_cursor[position]
165
+ end
166
+
167
+ def close
168
+ @raw_cursor.close
169
+ end
170
+
171
+ end
172
+
173
+ def select(sql, name = nil, return_column_names = false)
174
+ cursor = @raw_connection.exec(sql)
175
+ cols = []
176
+ # Ignore raw_rnum_ which is used to simulate LIMIT and OFFSET
177
+ cursor.get_col_names.each do |col_name|
178
+ col_name = oracle_downcase(col_name)
179
+ cols << col_name unless col_name == 'raw_rnum_'
180
+ end
181
+ # Reuse the same hash for all rows
182
+ column_hash = {}
183
+ cols.each {|c| column_hash[c] = nil}
184
+ rows = []
185
+ get_lob_value = !(name == 'Writable Large Object')
186
+
187
+ while row = cursor.fetch
188
+ hash = column_hash.dup
189
+
190
+ cols.each_with_index do |col, i|
191
+ hash[col] = typecast_result_value(row[i], get_lob_value)
192
+ end
193
+
194
+ rows << hash
195
+ end
196
+
197
+ return_column_names ? [rows, cols] : rows
198
+ ensure
199
+ cursor.close if cursor
200
+ end
201
+
202
+ def write_lob(lob, value, is_binary = false)
203
+ lob.write value
204
+ end
205
+
206
+ def describe(name)
207
+ # fall back to SELECT based describe if using database link
208
+ return super if name.to_s.include?('@')
209
+ quoted_name = OracleEnhancedAdapter.valid_table_name?(name) ? name : "\"#{name}\""
210
+ @raw_connection.describe(quoted_name)
211
+ rescue OCIException => e
212
+ # fall back to SELECT which can handle synonyms to database links
213
+ super
214
+ end
215
+
216
+ # Return OCIError error code
217
+ def error_code(exception)
218
+ case exception
219
+ when OCIError
220
+ exception.code
221
+ else
222
+ nil
223
+ end
224
+ end
225
+
226
+ def typecast_result_value(value, get_lob_value)
227
+ case value
228
+ when Fixnum, Bignum
229
+ value
230
+ when String
231
+ value
232
+ when Float, BigDecimal
233
+ # return Fixnum or Bignum if value is integer (to avoid issues with _before_type_cast values for id attributes)
234
+ value == (v_to_i = value.to_i) ? v_to_i : value
235
+ when OraNumber
236
+ # change OraNumber value (returned in early versions of ruby-oci8 2.0.x) to BigDecimal
237
+ value == (v_to_i = value.to_i) ? v_to_i : BigDecimal.new(value.to_s)
238
+ when OCI8::LOB
239
+ if get_lob_value
240
+ data = value.read || "" # if value.read returns nil, then we have an empty_clob() i.e. an empty string
241
+ # In Ruby 1.9.1 always change encoding to ASCII-8BIT for binaries
242
+ data.force_encoding('ASCII-8BIT') if data.respond_to?(:force_encoding) && value.is_a?(OCI8::BLOB)
243
+ data
244
+ else
245
+ value
246
+ end
247
+ # ruby-oci8 1.0 returns OraDate
248
+ # ruby-oci8 2.0 returns Time or DateTime
249
+ when OraDate, Time, DateTime
250
+ if OracleEnhancedAdapter.emulate_dates && date_without_time?(value)
251
+ value.to_date
252
+ else
253
+ create_time_with_default_timezone(value)
254
+ end
255
+ else
256
+ value
257
+ end
258
+ end
259
+
260
+ private
261
+
262
+ def date_without_time?(value)
263
+ case value
264
+ when OraDate
265
+ value.hour == 0 && value.minute == 0 && value.second == 0
266
+ else
267
+ value.hour == 0 && value.min == 0 && value.sec == 0
268
+ end
269
+ end
270
+
271
+ def create_time_with_default_timezone(value)
272
+ year, month, day, hour, min, sec, usec = case value
273
+ when Time
274
+ [value.year, value.month, value.day, value.hour, value.min, value.sec, value.usec]
275
+ when OraDate
276
+ [value.year, value.month, value.day, value.hour, value.minute, value.second, 0]
277
+ else
278
+ [value.year, value.month, value.day, value.hour, value.min, value.sec, 0]
279
+ end
280
+ # code from Time.time_with_datetime_fallback
281
+ begin
282
+ Time.send(Base.default_timezone, year, month, day, hour, min, sec, usec)
283
+ rescue
284
+ offset = Base.default_timezone.to_sym == :local ? ::DateTime.local_offset : 0
285
+ ::DateTime.civil(year, month, day, hour, min, sec, offset)
286
+ end
287
+ end
288
+
289
+ end
290
+
291
+ # The OracleEnhancedOCIFactory factors out the code necessary to connect and
292
+ # configure an Oracle/OCI connection.
293
+ class OracleEnhancedOCIFactory #:nodoc:
294
+ def self.new_connection(config)
295
+ # to_s needed if username, password or database is specified as number in database.yml file
296
+ username = config[:username] && config[:username].to_s
297
+ password = config[:password] && config[:password].to_s
298
+ database = config[:database] && config[:database].to_s
299
+ host, port = config[:host], config[:port]
300
+ privilege = config[:privilege] && config[:privilege].to_sym
301
+ async = config[:allow_concurrency]
302
+ prefetch_rows = config[:prefetch_rows] || 100
303
+ cursor_sharing = config[:cursor_sharing] || 'force'
304
+ # get session time_zone from configuration or from TZ environment variable
305
+ time_zone = config[:time_zone] || ENV['TZ']
306
+
307
+ # connection using host, port and database name
308
+ connection_string = if host || port
309
+ host ||= 'localhost'
310
+ host = "[#{host}]" if host =~ /^[^\[].*:/ # IPv6
311
+ port ||= 1521
312
+ database = "/#{database}" unless database.match(/^\//)
313
+ "//#{host}:#{port}#{database}"
314
+ # if no host is specified then assume that
315
+ # database parameter is TNS alias or TNS connection string
316
+ else
317
+ database
318
+ end
319
+
320
+ conn = OCI8.new username, password, connection_string, privilege
321
+ conn.autocommit = true
322
+ conn.non_blocking = true if async
323
+ conn.prefetch_rows = prefetch_rows
324
+ conn.exec "alter session set cursor_sharing = #{cursor_sharing}" rescue nil
325
+ conn.exec "alter session set time_zone = '#{time_zone}'" unless time_zone.blank?
326
+
327
+ # Initialize NLS parameters
328
+ OracleEnhancedAdapter::DEFAULT_NLS_PARAMETERS.each do |key, default_value|
329
+ value = config[key] || ENV[key.to_s.upcase] || default_value
330
+ if value
331
+ conn.exec "alter session set #{key} = '#{value}'"
332
+ end
333
+ end
334
+ conn
335
+ end
336
+ end
337
+
338
+
339
+ end
340
+ end
341
+
342
+
343
+
344
+ class OCI8 #:nodoc:
345
+
346
+ class Cursor #:nodoc:
347
+ if method_defined? :define_a_column
348
+ # This OCI8 patch is required with the ruby-oci8 1.0.x or lower.
349
+ # Set OCI8::BindType::Mapping[] to change the column type
350
+ # when using ruby-oci8 2.0.
351
+
352
+ alias :enhanced_define_a_column_pre_ar :define_a_column
353
+ def define_a_column(i)
354
+ case do_ocicall(@ctx) { @parms[i - 1].attrGet(OCI_ATTR_DATA_TYPE) }
355
+ when 8; @stmt.defineByPos(i, String, 65535) # Read LONG values
356
+ when 187; @stmt.defineByPos(i, OraDate) # Read TIMESTAMP values
357
+ when 108
358
+ if @parms[i - 1].attrGet(OCI_ATTR_TYPE_NAME) == 'XMLTYPE'
359
+ @stmt.defineByPos(i, String, 65535)
360
+ else
361
+ raise 'unsupported datatype'
362
+ end
363
+ else enhanced_define_a_column_pre_ar i
364
+ end
365
+ end
366
+ end
367
+ end
368
+
369
+ if OCI8.public_method_defined?(:describe_table)
370
+ # ruby-oci8 2.0 or upper
371
+
372
+ def describe(name)
373
+ info = describe_table(name.to_s)
374
+ raise %Q{"DESC #{name}" failed} if info.nil?
375
+ [info.obj_schema, info.obj_name]
376
+ end
377
+ else
378
+ # ruby-oci8 1.0.x or lower
379
+
380
+ # missing constant from oci8 < 0.1.14
381
+ OCI_PTYPE_UNK = 0 unless defined?(OCI_PTYPE_UNK)
382
+
383
+ # Uses the describeAny OCI call to find the target owner and table_name
384
+ # indicated by +name+, parsing through synonynms as necessary. Returns
385
+ # an array of [owner, table_name].
386
+ def describe(name)
387
+ @desc ||= @@env.alloc(OCIDescribe)
388
+ @desc.attrSet(OCI_ATTR_DESC_PUBLIC, -1) if VERSION >= '0.1.14'
389
+ do_ocicall(@ctx) { @desc.describeAny(@svc, name.to_s, OCI_PTYPE_UNK) } rescue raise %Q{"DESC #{name}" failed; does it exist?}
390
+ info = @desc.attrGet(OCI_ATTR_PARAM)
391
+
392
+ case info.attrGet(OCI_ATTR_PTYPE)
393
+ when OCI_PTYPE_TABLE, OCI_PTYPE_VIEW
394
+ owner = info.attrGet(OCI_ATTR_OBJ_SCHEMA)
395
+ table_name = info.attrGet(OCI_ATTR_OBJ_NAME)
396
+ [owner, table_name]
397
+ when OCI_PTYPE_SYN
398
+ schema = info.attrGet(OCI_ATTR_SCHEMA_NAME)
399
+ name = info.attrGet(OCI_ATTR_NAME)
400
+ describe(schema + '.' + name)
401
+ else raise %Q{"DESC #{name}" failed; not a table or view.}
402
+ end
403
+ end
404
+ end
405
+
406
+ end
407
+
408
+ # The OCI8AutoRecover class enhances the OCI8 driver with auto-recover and
409
+ # reset functionality. If a call to #exec fails, and autocommit is turned on
410
+ # (ie., we're not in the middle of a longer transaction), it will
411
+ # automatically reconnect and try again. If autocommit is turned off,
412
+ # this would be dangerous (as the earlier part of the implied transaction
413
+ # may have failed silently if the connection died) -- so instead the
414
+ # connection is marked as dead, to be reconnected on it's next use.
415
+ #:stopdoc:
416
+ class OCI8EnhancedAutoRecover < DelegateClass(OCI8) #:nodoc:
417
+ attr_accessor :active #:nodoc:
418
+ alias :active? :active #:nodoc:
419
+
420
+ cattr_accessor :auto_retry
421
+ class << self
422
+ alias :auto_retry? :auto_retry #:nodoc:
423
+ end
424
+ @@auto_retry = false
425
+
426
+ def initialize(config, factory) #:nodoc:
427
+ @active = true
428
+ @config = config
429
+ @factory = factory
430
+ @connection = @factory.new_connection @config
431
+ super @connection
432
+ end
433
+
434
+ # Checks connection, returns true if active. Note that ping actively
435
+ # checks the connection, while #active? simply returns the last
436
+ # known state.
437
+ def ping #:nodoc:
438
+ @connection.exec("select 1 from dual") { |r| nil }
439
+ @active = true
440
+ rescue
441
+ @active = false
442
+ raise
443
+ end
444
+
445
+ # Resets connection, by logging off and creating a new connection.
446
+ def reset! #:nodoc:
447
+ logoff rescue nil
448
+ begin
449
+ @connection = @factory.new_connection @config
450
+ __setobj__ @connection
451
+ @active = true
452
+ rescue
453
+ @active = false
454
+ raise
455
+ end
456
+ end
457
+
458
+ # ORA-00028: your session has been killed
459
+ # ORA-01012: not logged on
460
+ # ORA-03113: end-of-file on communication channel
461
+ # ORA-03114: not connected to ORACLE
462
+ # ORA-03135: connection lost contact
463
+ LOST_CONNECTION_ERROR_CODES = [ 28, 1012, 3113, 3114, 3135 ] #:nodoc:
464
+
465
+ # Adds auto-recovery functionality.
466
+ #
467
+ # See: http://www.jiubao.org/ruby-oci8/api.en.html#label-11
468
+ def exec(sql, *bindvars, &block) #:nodoc:
469
+ should_retry = self.class.auto_retry? && autocommit?
470
+
471
+ begin
472
+ @connection.exec(sql, *bindvars, &block)
473
+ rescue OCIException => e
474
+ raise unless e.is_a?(OCIError) && LOST_CONNECTION_ERROR_CODES.include?(e.code)
475
+ @active = false
476
+ raise unless should_retry
477
+ should_retry = false
478
+ reset! rescue nil
479
+ retry
480
+ end
481
+ end
482
+
483
+ # otherwise not working in Ruby 1.9.1
484
+ if RUBY_VERSION =~ /^1\.9/
485
+ def describe(name) #:nodoc:
486
+ @connection.describe(name)
487
+ end
488
+ end
489
+
490
+ end
491
+ #:startdoc: