pmacs-activerecord-oracle_enhanced-adapter 1.4.2.rc1

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