ctreatma-activerecord-oracle_enhanced-adapter 1.4.1.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.
Files changed (47) hide show
  1. data/.rspec +2 -0
  2. data/Gemfile +51 -0
  3. data/History.md +269 -0
  4. data/License.txt +20 -0
  5. data/README.md +378 -0
  6. data/RUNNING_TESTS.md +45 -0
  7. data/Rakefile +46 -0
  8. data/VERSION +1 -0
  9. data/activerecord-oracle_enhanced-adapter.gemspec +130 -0
  10. data/ctreatma-activerecord-oracle_enhanced-adapter.gemspec +129 -0
  11. data/lib/active_record/connection_adapters/emulation/oracle_adapter.rb +5 -0
  12. data/lib/active_record/connection_adapters/oracle_enhanced.rake +105 -0
  13. data/lib/active_record/connection_adapters/oracle_enhanced_activerecord_patches.rb +41 -0
  14. data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +1390 -0
  15. data/lib/active_record/connection_adapters/oracle_enhanced_base_ext.rb +106 -0
  16. data/lib/active_record/connection_adapters/oracle_enhanced_column.rb +136 -0
  17. data/lib/active_record/connection_adapters/oracle_enhanced_connection.rb +119 -0
  18. data/lib/active_record/connection_adapters/oracle_enhanced_context_index.rb +328 -0
  19. data/lib/active_record/connection_adapters/oracle_enhanced_core_ext.rb +25 -0
  20. data/lib/active_record/connection_adapters/oracle_enhanced_cpk.rb +21 -0
  21. data/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb +39 -0
  22. data/lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb +553 -0
  23. data/lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb +492 -0
  24. data/lib/active_record/connection_adapters/oracle_enhanced_procedures.rb +260 -0
  25. data/lib/active_record/connection_adapters/oracle_enhanced_schema_definitions.rb +213 -0
  26. data/lib/active_record/connection_adapters/oracle_enhanced_schema_dumper.rb +252 -0
  27. data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements.rb +373 -0
  28. data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements_ext.rb +265 -0
  29. data/lib/active_record/connection_adapters/oracle_enhanced_structure_dump.rb +290 -0
  30. data/lib/active_record/connection_adapters/oracle_enhanced_tasks.rb +17 -0
  31. data/lib/active_record/connection_adapters/oracle_enhanced_version.rb +1 -0
  32. data/lib/activerecord-oracle_enhanced-adapter.rb +25 -0
  33. data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +749 -0
  34. data/spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb +310 -0
  35. data/spec/active_record/connection_adapters/oracle_enhanced_context_index_spec.rb +426 -0
  36. data/spec/active_record/connection_adapters/oracle_enhanced_core_ext_spec.rb +19 -0
  37. data/spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb +113 -0
  38. data/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +1330 -0
  39. data/spec/active_record/connection_adapters/oracle_enhanced_dbms_output_spec.rb +69 -0
  40. data/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb +121 -0
  41. data/spec/active_record/connection_adapters/oracle_enhanced_emulate_oracle_adapter_spec.rb +25 -0
  42. data/spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb +374 -0
  43. data/spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb +380 -0
  44. data/spec/active_record/connection_adapters/oracle_enhanced_schema_statements_spec.rb +1112 -0
  45. data/spec/active_record/connection_adapters/oracle_enhanced_structure_dump_spec.rb +323 -0
  46. data/spec/spec_helper.rb +185 -0
  47. metadata +287 -0
@@ -0,0 +1,492 @@
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
+ schema = config[:schema]
307
+
308
+ # connection using host, port and database name
309
+ connection_string = if host || port
310
+ host ||= 'localhost'
311
+ host = "[#{host}]" if host =~ /^[^\[].*:/ # IPv6
312
+ port ||= 1521
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
+ conn.exec "alter session set current_schema = #{schema}" unless schema.blank?
327
+
328
+ # Initialize NLS parameters
329
+ OracleEnhancedAdapter::DEFAULT_NLS_PARAMETERS.each do |key, default_value|
330
+ value = config[key] || ENV[key.to_s.upcase] || default_value
331
+ if value
332
+ conn.exec "alter session set #{key} = '#{value}'"
333
+ end
334
+ end
335
+ conn
336
+ end
337
+ end
338
+
339
+
340
+ end
341
+ end
342
+
343
+
344
+
345
+ class OCI8 #:nodoc:
346
+
347
+ class Cursor #:nodoc:
348
+ if method_defined? :define_a_column
349
+ # This OCI8 patch is required with the ruby-oci8 1.0.x or lower.
350
+ # Set OCI8::BindType::Mapping[] to change the column type
351
+ # when using ruby-oci8 2.0.
352
+
353
+ alias :enhanced_define_a_column_pre_ar :define_a_column
354
+ def define_a_column(i)
355
+ case do_ocicall(@ctx) { @parms[i - 1].attrGet(OCI_ATTR_DATA_TYPE) }
356
+ when 8; @stmt.defineByPos(i, String, 65535) # Read LONG values
357
+ when 187; @stmt.defineByPos(i, OraDate) # Read TIMESTAMP values
358
+ when 108
359
+ if @parms[i - 1].attrGet(OCI_ATTR_TYPE_NAME) == 'XMLTYPE'
360
+ @stmt.defineByPos(i, String, 65535)
361
+ else
362
+ raise 'unsupported datatype'
363
+ end
364
+ else enhanced_define_a_column_pre_ar i
365
+ end
366
+ end
367
+ end
368
+ end
369
+
370
+ if OCI8.public_method_defined?(:describe_table)
371
+ # ruby-oci8 2.0 or upper
372
+
373
+ def describe(name)
374
+ info = describe_table(name.to_s)
375
+ raise %Q{"DESC #{name}" failed} if info.nil?
376
+ [info.obj_schema, info.obj_name]
377
+ end
378
+ else
379
+ # ruby-oci8 1.0.x or lower
380
+
381
+ # missing constant from oci8 < 0.1.14
382
+ OCI_PTYPE_UNK = 0 unless defined?(OCI_PTYPE_UNK)
383
+
384
+ # Uses the describeAny OCI call to find the target owner and table_name
385
+ # indicated by +name+, parsing through synonynms as necessary. Returns
386
+ # an array of [owner, table_name].
387
+ def describe(name)
388
+ @desc ||= @@env.alloc(OCIDescribe)
389
+ @desc.attrSet(OCI_ATTR_DESC_PUBLIC, -1) if VERSION >= '0.1.14'
390
+ do_ocicall(@ctx) { @desc.describeAny(@svc, name.to_s, OCI_PTYPE_UNK) } rescue raise %Q{"DESC #{name}" failed; does it exist?}
391
+ info = @desc.attrGet(OCI_ATTR_PARAM)
392
+
393
+ case info.attrGet(OCI_ATTR_PTYPE)
394
+ when OCI_PTYPE_TABLE, OCI_PTYPE_VIEW
395
+ owner = info.attrGet(OCI_ATTR_OBJ_SCHEMA)
396
+ table_name = info.attrGet(OCI_ATTR_OBJ_NAME)
397
+ [owner, table_name]
398
+ when OCI_PTYPE_SYN
399
+ schema = info.attrGet(OCI_ATTR_SCHEMA_NAME)
400
+ name = info.attrGet(OCI_ATTR_NAME)
401
+ describe(schema + '.' + name)
402
+ else raise %Q{"DESC #{name}" failed; not a table or view.}
403
+ end
404
+ end
405
+ end
406
+
407
+ end
408
+
409
+ # The OCI8AutoRecover class enhances the OCI8 driver with auto-recover and
410
+ # reset functionality. If a call to #exec fails, and autocommit is turned on
411
+ # (ie., we're not in the middle of a longer transaction), it will
412
+ # automatically reconnect and try again. If autocommit is turned off,
413
+ # this would be dangerous (as the earlier part of the implied transaction
414
+ # may have failed silently if the connection died) -- so instead the
415
+ # connection is marked as dead, to be reconnected on it's next use.
416
+ #:stopdoc:
417
+ class OCI8EnhancedAutoRecover < DelegateClass(OCI8) #:nodoc:
418
+ attr_accessor :active #:nodoc:
419
+ alias :active? :active #:nodoc:
420
+
421
+ cattr_accessor :auto_retry
422
+ class << self
423
+ alias :auto_retry? :auto_retry #:nodoc:
424
+ end
425
+ @@auto_retry = false
426
+
427
+ def initialize(config, factory) #:nodoc:
428
+ @active = true
429
+ @config = config
430
+ @factory = factory
431
+ @connection = @factory.new_connection @config
432
+ super @connection
433
+ end
434
+
435
+ # Checks connection, returns true if active. Note that ping actively
436
+ # checks the connection, while #active? simply returns the last
437
+ # known state.
438
+ def ping #:nodoc:
439
+ @connection.exec("select 1 from dual") { |r| nil }
440
+ @active = true
441
+ rescue
442
+ @active = false
443
+ raise
444
+ end
445
+
446
+ # Resets connection, by logging off and creating a new connection.
447
+ def reset! #:nodoc:
448
+ logoff rescue nil
449
+ begin
450
+ @connection = @factory.new_connection @config
451
+ __setobj__ @connection
452
+ @active = true
453
+ rescue
454
+ @active = false
455
+ raise
456
+ end
457
+ end
458
+
459
+ # ORA-00028: your session has been killed
460
+ # ORA-01012: not logged on
461
+ # ORA-03113: end-of-file on communication channel
462
+ # ORA-03114: not connected to ORACLE
463
+ # ORA-03135: connection lost contact
464
+ LOST_CONNECTION_ERROR_CODES = [ 28, 1012, 3113, 3114, 3135 ] #:nodoc:
465
+
466
+ # Adds auto-recovery functionality.
467
+ #
468
+ # See: http://www.jiubao.org/ruby-oci8/api.en.html#label-11
469
+ def exec(sql, *bindvars, &block) #:nodoc:
470
+ should_retry = self.class.auto_retry? && autocommit?
471
+
472
+ begin
473
+ @connection.exec(sql, *bindvars, &block)
474
+ rescue OCIException => e
475
+ raise unless e.is_a?(OCIError) && LOST_CONNECTION_ERROR_CODES.include?(e.code)
476
+ @active = false
477
+ raise unless should_retry
478
+ should_retry = false
479
+ reset! rescue nil
480
+ retry
481
+ end
482
+ end
483
+
484
+ # otherwise not working in Ruby 1.9.1
485
+ if RUBY_VERSION =~ /^1\.9/
486
+ def describe(name) #:nodoc:
487
+ @connection.describe(name)
488
+ end
489
+ end
490
+
491
+ end
492
+ #:startdoc: