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