Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

root/adapters/oracle/lib/active_record/connection_adapters/oracle_adapter.rb

Revision 9181, 27.2 kB (checked in by bitsweat, 2 years ago)

Add select_rows. Closes #11334 [nwiger]

Line 
1 # oracle_adapter.rb -- ActiveRecord adapter for Oracle 8i, 9i, 10g
2 #
3 # Original author: Graham Jenkins
4 #
5 # Current maintainer: Michael Schoen <schoenm@earthlink.net>
6 #
7 #########################################################################
8 #
9 # Implementation notes:
10 # 1. Redefines (safely) a method in ActiveRecord to make it possible to
11 #    implement an autonumbering solution for Oracle.
12 # 2. The OCI8 driver is patched to properly handle values for LONG and
13 #    TIMESTAMP columns. The driver-author has indicated that a future
14 #    release of the driver will obviate this patch.
15 # 3. LOB support is implemented through an after_save callback.
16 # 4. Oracle does not offer native LIMIT and OFFSET options; this
17 #    functionality is mimiced through the use of nested selects.
18 #    See http://asktom.oracle.com/pls/ask/f?p=4950:8:::::F4950_P8_DISPLAYID:127412348064
19 #
20 # Do what you want with this code, at your own peril, but if any
21 # significant portion of my code remains then please acknowledge my
22 # contribution.
23 # portions Copyright 2005 Graham Jenkins
24
25 require 'active_record/connection_adapters/abstract_adapter'
26 require 'delegate'
27
28 begin
29   require_library_or_gem 'oci8' unless self.class.const_defined? :OCI8
30
31   module ActiveRecord
32     class Base
33       def self.oracle_connection(config) #:nodoc:
34         # Use OCI8AutoRecover instead of normal OCI8 driver.
35         ConnectionAdapters::OracleAdapter.new OCI8AutoRecover.new(config), logger
36       end
37
38       # for backwards-compatibility
39       def self.oci_connection(config) #:nodoc:
40         config[:database] = config[:host]
41         self.oracle_connection(config)
42       end
43
44       # After setting large objects to empty, select the OCI8::LOB
45       # and write back the data.
46       after_save :write_lobs
47       def write_lobs #:nodoc:
48         if connection.is_a?(ConnectionAdapters::OracleAdapter)
49           connection.write_lobs(self.class.table_name, self.class, attributes)
50         end
51       end
52
53       private :write_lobs
54     end
55
56
57     module ConnectionAdapters #:nodoc:
58       class OracleColumn < Column #:nodoc:
59
60         def type_cast(value)
61           return guess_date_or_time(value) if type == :datetime && OracleAdapter.emulate_dates
62           super
63         end
64
65         private
66         def simplified_type(field_type)
67           return :boolean if OracleAdapter.emulate_booleans && field_type == 'NUMBER(1)'
68           case field_type
69             when /date|time/i then :datetime
70             else super
71           end
72         end
73
74         def guess_date_or_time(value)
75           (value.hour == 0 and value.min == 0 and value.sec == 0) ?
76             Date.new(value.year, value.month, value.day) : value
77         end
78       end
79
80
81       # This is an Oracle/OCI adapter for the ActiveRecord persistence
82       # framework. It relies upon the OCI8 driver, which works with Oracle 8i
83       # and above. Most recent development has been on Debian Linux against
84       # a 10g database, ActiveRecord 1.12.1 and OCI8 0.1.13.
85       # See: http://rubyforge.org/projects/ruby-oci8/
86       #
87       # Usage notes:
88       # * Key generation assumes a "${table_name}_seq" sequence is available
89       #   for all tables; the sequence name can be changed using
90       #   ActiveRecord::Base.set_sequence_name. When using Migrations, these
91       #   sequences are created automatically.
92       # * Oracle uses DATE or TIMESTAMP datatypes for both dates and times.
93       #   Consequently some hacks are employed to map data back to Date or Time
94       #   in Ruby. If the column_name ends in _time it's created as a Ruby Time.
95       #   Else if the hours/minutes/seconds are 0, I make it a Ruby Date. Else
96       #   it's a Ruby Time. This is a bit nasty - but if you use Duck Typing
97       #   you'll probably not care very much. In 9i and up it's tempting to
98       #   map DATE to Date and TIMESTAMP to Time, but too many databases use
99       #   DATE for both. Timezones and sub-second precision on timestamps are
100       #   not supported.
101       # * Default values that are functions (such as "SYSDATE") are not
102       #   supported. This is a restriction of the way ActiveRecord supports
103       #   default values.
104       # * Support for Oracle8 is limited by Rails' use of ANSI join syntax, which
105       #   is supported in Oracle9i and later. You will need to use #finder_sql for
106       #   has_and_belongs_to_many associations to run against Oracle8.
107       #
108       # Required parameters:
109       #
110       # * <tt>:username</tt>
111       # * <tt>:password</tt>
112       # * <tt>:database</tt>
113       class OracleAdapter < AbstractAdapter
114
115         @@emulate_booleans = true
116         cattr_accessor :emulate_booleans
117
118         @@emulate_dates = false
119         cattr_accessor :emulate_dates
120
121         def adapter_name #:nodoc:
122           'Oracle'
123         end
124
125         def supports_migrations? #:nodoc:
126           true
127         end
128
129         def native_database_types #:nodoc:
130           {
131             :primary_key => "NUMBER(38) NOT NULL PRIMARY KEY",
132             :string      => { :name => "VARCHAR2", :limit => 255 },
133             :text        => { :name => "CLOB" },
134             :integer     => { :name => "NUMBER", :limit => 38 },
135             :float       => { :name => "NUMBER" },
136             :decimal     => { :name => "DECIMAL" },
137             :datetime    => { :name => "DATE" },
138             :timestamp   => { :name => "DATE" },
139             :time        => { :name => "DATE" },
140             :date        => { :name => "DATE" },
141             :binary      => { :name => "BLOB" },
142             :boolean     => { :name => "NUMBER", :limit => 1 }
143           }
144         end
145
146         def table_alias_length
147           30
148         end
149
150         # Returns an array of arrays containing the field values.
151         # Order is the same as that returned by #columns.
152         def select_rows(sql, name = nil)
153           result = select(sql, name)
154           result.map{ |v| v.values}
155         end
156
157         # QUOTING ==================================================
158         #
159         # see: abstract/quoting.rb
160
161         # camelCase column names need to be quoted; not that anyone using Oracle
162         # would really do this, but handling this case means we pass the test...
163         def quote_column_name(name) #:nodoc:
164           name.to_s =~ /[A-Z]/ ? "\"#{name}\"" : name
165         end
166
167         def quote_string(s) #:nodoc:
168           s.gsub(/'/, "''")
169         end
170
171         def quote(value, column = nil) #:nodoc:
172           if value && column && [:text, :binary].include?(column.type)
173             %Q{empty_#{ column.sql_type.downcase rescue 'blob' }()}
174           else
175             super
176           end
177         end
178
179         def quoted_true
180           "1"
181         end
182
183         def quoted_false
184           "0"
185         end
186
187
188         # CONNECTION MANAGEMENT ====================================
189         #
190
191         # Returns true if the connection is active.
192         def active?
193           # Pings the connection to check if it's still good. Note that an
194           # #active? method is also available, but that simply returns the
195           # last known state, which isn't good enough if the connection has
196           # gone stale since the last use.
197           @connection.ping
198         rescue OCIException
199           false
200         end
201
202         # Reconnects to the database.
203         def reconnect!
204           @connection.reset!
205         rescue OCIException => e
206           @logger.warn "#{adapter_name} automatic reconnection failed: #{e.message}"
207         end
208
209         # Disconnects from the database.
210         def disconnect!
211           @connection.logoff rescue nil
212           @connection.active = false
213         end
214
215
216         # DATABASE STATEMENTS ======================================
217         #
218         # see: abstract/database_statements.rb
219
220         def execute(sql, name = nil) #:nodoc:
221           log(sql, name) { @connection.exec sql }
222         end
223
224         # Returns the next sequence value from a sequence generator. Not generally
225         # called directly; used by ActiveRecord to get the next primary key value
226         # when inserting a new database record (see #prefetch_primary_key?).
227         def next_sequence_value(sequence_name)
228           id = 0
229           @connection.exec("select #{sequence_name}.nextval id from dual") { |r| id = r[0].to_i }
230           id
231         end
232
233         def begin_db_transaction #:nodoc:
234           @connection.autocommit = false
235         end
236
237         def commit_db_transaction #:nodoc:
238           @connection.commit
239         ensure
240           @connection.autocommit = true
241         end
242
243         def rollback_db_transaction #:nodoc:
244           @connection.rollback
245         ensure
246           @connection.autocommit = true
247         end
248
249         def add_limit_offset!(sql, options) #:nodoc:
250           offset = options[:offset] || 0
251
252           if limit = options[:limit]
253             sql.replace "select * from (select raw_sql_.*, rownum raw_rnum_ from (#{sql}) raw_sql_ where rownum <= #{offset+limit}) where raw_rnum_ > #{offset}"
254           elsif offset > 0
255             sql.replace "select * from (select raw_sql_.*, rownum raw_rnum_ from (#{sql}) raw_sql_) where raw_rnum_ > #{offset}"
256           end
257         end
258
259         # Returns true for Oracle adapter (since Oracle requires primary key
260         # values to be pre-fetched before insert). See also #next_sequence_value.
261         def prefetch_primary_key?(table_name = nil)
262           true
263         end
264
265         def default_sequence_name(table, column) #:nodoc:
266           "#{table}_seq"
267         end
268
269
270         # Inserts the given fixture into the table. Overridden to properly handle lobs.
271         def insert_fixture(fixture, table_name)
272           super
273
274           klass = fixture.class_name.constantize rescue nil
275           if klass.respond_to?(:ancestors) && klass.ancestors.include?(ActiveRecord::Base)
276             write_lobs(table_name, klass, fixture)
277           end
278         end
279
280         # Writes LOB values from attributes, as indicated by the LOB columns of klass.
281         def write_lobs(table_name, klass, attributes)
282           id = quote(attributes[klass.primary_key])
283           klass.columns.select { |col| col.sql_type =~ /LOB$/i }.each do |col|
284             value = attributes[col.name]
285             value = value.to_yaml if col.text? && klass.serialized_attributes[col.name]
286             next if value.nil?  || (value == '')
287             lob = select_one("SELECT #{col.name} FROM #{table_name} WHERE #{klass.primary_key} = #{id}",
288                              'Writable Large Object')[col.name]
289             lob.write value
290           end
291         end
292
293
294         # SCHEMA STATEMENTS ========================================
295         #
296         # see: abstract/schema_statements.rb
297
298         def current_database #:nodoc:
299           select_one("select sys_context('userenv','db_name') db from dual")["db"]
300         end
301
302         def tables(name = nil) #:nodoc:
303           select_all("select lower(table_name) from user_tables").inject([]) do | tabs, t |
304             tabs << t.to_a.first.last
305           end
306         end
307
308         def indexes(table_name, name = nil) #:nodoc:
309           result = select_all(<<-SQL, name)
310             SELECT lower(i.index_name) as index_name, i.uniqueness, lower(c.column_name) as column_name
311               FROM user_indexes i, user_ind_columns c
312              WHERE i.table_name = '#{table_name.to_s.upcase}'
313                AND c.index_name = i.index_name
314                AND i.index_name NOT IN (SELECT uc.index_name FROM user_constraints uc WHERE uc.constraint_type = 'P')
315               ORDER BY i.index_name, c.column_position
316           SQL
317
318           current_index = nil
319           indexes = []
320
321           result.each do |row|
322             if current_index != row['index_name']
323               indexes << IndexDefinition.new(table_name, row['index_name'], row['uniqueness'] == "UNIQUE", [])
324               current_index = row['index_name']
325             end
326
327             indexes.last.columns << row['column_name']
328           end
329
330           indexes
331         end
332
333         def columns(table_name, name = nil) #:nodoc:
334           (owner, table_name) = @connection.describe(table_name)
335
336           table_cols = <<-SQL
337             select column_name as name, data_type as sql_type, data_default, nullable,
338                    decode(data_type, 'NUMBER', data_precision,
339                                      'FLOAT', data_precision,
340                                      'VARCHAR2', data_length,
341                                      'CHAR', data_length,
342                                       null) as limit,
343                    decode(data_type, 'NUMBER', data_scale, null) as scale
344               from all_tab_columns
345              where owner      = '#{owner}'
346                and table_name = '#{table_name}'
347              order by column_id
348           SQL
349
350           select_all(table_cols, name).map do |row|
351             limit, scale = row['limit'], row['scale']
352             if limit || scale
353               row['sql_type'] << "(#{(limit || 38).to_i}" + ((scale = scale.to_i) > 0 ? ",#{scale})" : ")")
354             end
355
356             # clean up odd default spacing from Oracle
357             if row['data_default']
358               row['data_default'].sub!(/^(.*?)\s*$/, '\1')
359               row['data_default'].sub!(/^'(.*)'$/, '\1')
360               row['data_default'] = nil if row['data_default'] =~ /^(null|empty_[bc]lob\(\))$/i
361             end
362
363             OracleColumn.new(oracle_downcase(row['name']),
364                              row['data_default'],
365                              row['sql_type'],
366                              row['nullable'] == 'Y')
367           end
368         end
369
370         def create_table(name, options = {}) #:nodoc:
371           super(name, options)
372           seq_name = options[:sequence_name] || "#{name}_seq"
373           execute "CREATE SEQUENCE #{seq_name} START WITH 10000" unless options[:id] == false
374         end
375
376         def rename_table(name, new_name) #:nodoc:
377           execute "RENAME #{name} TO #{new_name}"
378           execute "RENAME #{name}_seq TO #{new_name}_seq" rescue nil
379         end
380
381         def drop_table(name, options = {}) #:nodoc:
382           super(name)
383           seq_name = options[:sequence_name] || "#{name}_seq"
384           execute "DROP SEQUENCE #{seq_name}" rescue nil
385         end
386
387         def remove_index(table_name, options = {}) #:nodoc:
388           execute "DROP INDEX #{index_name(table_name, options)}"
389         end
390
391         def change_column_default(table_name, column_name, default) #:nodoc:
392           execute "ALTER TABLE #{table_name} MODIFY #{quote_column_name(column_name)} DEFAULT #{quote(default)}"
393         end
394
395         def change_column(table_name, column_name, type, options = {}) #:nodoc:
396           change_column_sql = "ALTER TABLE #{table_name} MODIFY #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
397           add_column_options!(change_column_sql, options)
398           execute(change_column_sql)
399         end
400
401         def rename_column(table_name, column_name, new_column_name) #:nodoc:
402           execute "ALTER TABLE #{table_name} RENAME COLUMN #{quote_column_name(column_name)} to #{quote_column_name(new_column_name)}"
403         end
404
405         def remove_column(table_name, column_name) #:nodoc:
406           execute "ALTER TABLE #{table_name} DROP COLUMN #{quote_column_name(column_name)}"
407         end
408
409         # Find a table's primary key and sequence.
410         # *Note*: Only primary key is implemented - sequence will be nil.
411         def pk_and_sequence_for(table_name)
412           (owner, table_name) = @connection.describe(table_name)
413
414           pks = select_values(<<-SQL, 'Primary Key')
415             select cc.column_name
416               from all_constraints c, all_cons_columns cc
417              where c.owner = '#{owner}'
418                and c.table_name = '#{table_name}'
419                and c.constraint_type = 'P'
420                and cc.owner = c.owner
421                and cc.constraint_name = c.constraint_name
422           SQL
423
424           # only support single column keys
425           pks.size == 1 ? [oracle_downcase(pks.first), nil] : nil
426         end
427
428         def structure_dump #:nodoc:
429           s = select_all("select sequence_name from user_sequences").inject("") do |structure, seq|
430             structure << "create sequence #{seq.to_a.first.last};\n\n"
431           end
432
433           select_all("select table_name from user_tables").inject(s) do |structure, table|
434             ddl = "create table #{table.to_a.first.last} (\n "
435             cols = select_all(%Q{
436               select column_name, data_type, data_length, data_precision, data_scale, data_default, nullable
437               from user_tab_columns
438               where table_name = '#{table.to_a.first.last}'
439               order by column_id
440             }).map do |row|
441               col = "#{row['column_name'].downcase} #{row['data_type'].downcase}"
442               if row['data_type'] =='NUMBER' and !row['data_precision'].nil?
443                 col << "(#{row['data_precision'].to_i}"
444                 col << ",#{row['data_scale'].to_i}" if !row['data_scale'].nil?
445                 col << ')'
446               elsif row['data_type'].include?('CHAR')
447                 col << "(#{row['data_length'].to_i})"
448               end
449               col << " default #{row['data_default']}" if !row['data_default'].nil?
450               col << ' not null' if row['nullable'] == 'N'
451               col
452             end
453             ddl << cols.join(",\n ")
454             ddl << ");\n\n"
455             structure << ddl
456           end
457         end
458
459         def structure_drop #:nodoc:
460           s = select_all("select sequence_name from user_sequences").inject("") do |drop, seq|
461             drop << "drop sequence #{seq.to_a.first.last};\n\n"
462           end
463
464           select_all("select table_name from user_tables").inject(s) do |drop, table|
465             drop << "drop table #{table.to_a.first.last} cascade constraints;\n\n"
466           end
467         end
468
469         def add_column_options!(sql, options) #:nodoc:
470           # handle case of defaults for CLOB columns, which would otherwise get "quoted" incorrectly
471           if options_include_default?(options) && (column = options[:column]) && column.type == :text
472             sql << " DEFAULT #{quote(options.delete(:default))}"
473           end
474           super
475         end
476
477         # SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
478         #
479         # Oracle requires the ORDER BY columns to be in the SELECT list for DISTINCT
480         # queries. However, with those columns included in the SELECT DISTINCT list, you
481         # won't actually get a distinct list of the column you want (presuming the column
482         # has duplicates with multiple values for the ordered-by columns. So we use the
483         # FIRST_VALUE function to get a single (first) value for each column, effectively
484         # making every row the same.
485         #
486         #   distinct("posts.id", "posts.created_at desc")
487         def distinct(columns, order_by)
488           return "DISTINCT #{columns}" if order_by.blank?
489
490           # construct a valid DISTINCT clause, ie. one that includes the ORDER BY columns, using
491           # FIRST_VALUE such that the inclusion of these columns doesn't invalidate the DISTINCT
492           order_columns = order_by.split(',').map { |s| s.strip }.reject(&:blank?)
493           order_columns = order_columns.zip((0...order_columns.size).to_a).map do |c, i|
494             "FIRST_VALUE(#{c.split.first}) OVER (PARTITION BY #{columns} ORDER BY #{c}) AS alias_#{i}__"
495           end
496           sql = "DISTINCT #{columns}, "
497           sql << order_columns * ", "
498         end
499
500         # ORDER BY clause for the passed order option.
501         #
502         # Uses column aliases as defined by #distinct.
503         def add_order_by_for_association_limiting!(sql, options)
504           return sql if options[:order].blank?
505
506           order = options[:order].split(',').collect { |s| s.strip }.reject(&:blank?)
507           order.map! {|s| $1 if s =~ / (.*)/}
508           order = order.zip((0...order.size).to_a).map { |s,i| "alias_#{i}__ #{s}" }.join(', ')
509
510           sql << " ORDER BY #{order}"
511         end
512
513         private
514
515         def select(sql, name = nil)
516           cursor = execute(sql, name)
517           cols = cursor.get_col_names.map { |x| oracle_downcase(x) }
518           rows = []
519
520           while row = cursor.fetch
521             hash = Hash.new
522
523             cols.each_with_index do |col, i|
524               hash[col] =
525                 case row[i]
526                 when OCI8::LOB
527                   name == 'Writable Large Object' ? row[i]: row[i].read
528                 when OraDate
529                   d = row[i]
530                   if emulate_dates && (d.hour == 0 && d.minute == 0 && d.second == 0)
531                     d.to_date
532                   else
533                     # see string_to_time; Time overflowing to DateTime, respecting the default timezone
534                     time_array = [d.year, d.month, d.day, d.hour, d.minute, d.second]
535                     begin
536                       Time.send(Base.default_timezone, *time_array)
537                     rescue
538                       zone_offset = if Base.default_timezone == :local then DateTime.now.offset else 0 end
539                       # Append zero calendar reform start to account for dates skipped by calendar reform
540                       DateTime.new(*time_array[0..5] << zone_offset << 0) rescue nil
541                     end
542                   end
543                 else row[i]
544                 end unless col == 'raw_rnum_'
545             end
546
547             rows << hash
548           end
549
550           rows
551         ensure
552           cursor.close if cursor
553         end
554
555         # Oracle column names by default are case-insensitive, but treated as upcase;
556         # for neatness, we'll downcase within Rails. EXCEPT that folks CAN quote
557         # their column names when creating Oracle tables, which makes then case-sensitive.
558         # I don't know anybody who does this, but we'll handle the theoretical case of a
559         # camelCase column name. I imagine other dbs handle this different, since there's a
560         # unit test that's currently failing test_oci.
561         def oracle_downcase(column_name)
562           column_name =~ /[a-z]/ ? column_name : column_name.downcase
563         end
564
565       end
566     end
567   end
568
569
570   class OCI8 #:nodoc:
571
572     # This OCI8 patch may not longer be required with the upcoming
573     # release of version 0.2.
574     class Cursor #:nodoc:
575       alias :define_a_column_pre_ar :define_a_column
576       def define_a_column(i)
577         case do_ocicall(@ctx) { @parms[i - 1].attrGet(OCI_ATTR_DATA_TYPE) }
578         when 8;   @stmt.defineByPos(i, String, 65535) # Read LONG values
579         when 187; @stmt.defineByPos(i, OraDate) # Read TIMESTAMP values
580         when 108
581           if @parms[i - 1].attrGet(OCI_ATTR_TYPE_NAME) == 'XMLTYPE'
582             @stmt.defineByPos(i, String, 65535)
583           else
584             raise 'unsupported datatype'
585           end
586         else define_a_column_pre_ar i
587         end
588       end
589     end
590
591     # missing constant from oci8 < 0.1.14
592     OCI_PTYPE_UNK = 0 unless defined?(OCI_PTYPE_UNK)
593
594     # Uses the describeAny OCI call to find the target owner and table_name
595     # indicated by +name+, parsing through synonynms as necessary. Returns
596     # an array of [owner, table_name].
597     def describe(name)
598       @desc ||= @@env.alloc(OCIDescribe)
599       @desc.attrSet(OCI_ATTR_DESC_PUBLIC, -1) if VERSION >= '0.1.14'
600       @desc.describeAny(@svc, name.to_s, OCI_PTYPE_UNK) rescue raise %Q{"DESC #{name}" failed; does it exist?}
601       info = @desc.attrGet(OCI_ATTR_PARAM)
602
603       case info.attrGet(OCI_ATTR_PTYPE)
604       when OCI_PTYPE_TABLE, OCI_PTYPE_VIEW
605         owner      = info.attrGet(OCI_ATTR_OBJ_SCHEMA)
606         table_name = info.attrGet(OCI_ATTR_OBJ_NAME)
607         [owner, table_name]
608       when OCI_PTYPE_SYN
609         schema = info.attrGet(OCI_ATTR_SCHEMA_NAME)
610         name   = info.attrGet(OCI_ATTR_NAME)
611         describe(schema + '.' + name)
612       else raise %Q{"DESC #{name}" failed; not a table or view.}
613       end
614     end
615
616   end
617
618
619   # The OracleConnectionFactory factors out the code necessary to connect and
620   # configure an Oracle/OCI connection.
621   class OracleConnectionFactory #:nodoc:
622     def new_connection(username, password, database, async, prefetch_rows, cursor_sharing)
623       conn = OCI8.new username, password, database
624       conn.exec %q{alter session set nls_date_format = 'YYYY-MM-DD HH24:MI:SS'}
625       conn.exec %q{alter session set nls_timestamp_format = 'YYYY-MM-DD HH24:MI:SS'} rescue nil
626       conn.autocommit = true
627       conn.non_blocking = true if async
628       conn.prefetch_rows = prefetch_rows
629       conn.exec "alter session set cursor_sharing = #{cursor_sharing}" rescue nil
630       conn
631     end
632   end
633
634
635   # The OCI8AutoRecover class enhances the OCI8 driver with auto-recover and
636   # reset functionality. If a call to #exec fails, and autocommit is turned on
637   # (ie., we're not in the middle of a longer transaction), it will
638   # automatically reconnect and try again. If autocommit is turned off,
639   # this would be dangerous (as the earlier part of the implied transaction
640   # may have failed silently if the connection died) -- so instead the
641   # connection is marked as dead, to be reconnected on it's next use.
642   class OCI8AutoRecover < DelegateClass(OCI8) #:nodoc:
643     attr_accessor :active
644     alias :active? :active
645
646     cattr_accessor :auto_retry
647     class << self
648       alias :auto_retry? :auto_retry
649     end
650     @@auto_retry = false
651
652     def initialize(config, factory = OracleConnectionFactory.new)
653       @active = true
654       @username, @password, @database, = config[:username].to_s, config[:password].to_s, config[:database].to_s
655       @async = config[:allow_concurrency]
656       @prefetch_rows = config[:prefetch_rows] || 100
657       @cursor_sharing = config[:cursor_sharing] || 'similar'
658       @factory = factory
659       @connection  = @factory.new_connection @username, @password, @database, @async, @prefetch_rows, @cursor_sharing
660       super @connection
661     end
662
663     # Checks connection, returns true if active. Note that ping actively
664     # checks the connection, while #active? simply returns the last
665     # known state.
666     def ping
667       @connection.exec("select 1 from dual") { |r| nil }
668       @active = true
669     rescue
670       @active = false
671       raise
672     end
673
674     # Resets connection, by logging off and creating a new connection.
675     def reset!
676       logoff rescue nil
677       begin
678         @connection = @factory.new_connection @username, @password, @database, @async, @prefetch_rows, @cursor_sharing
679         __setobj__ @connection
680         @active = true
681       rescue
682         @active = false
683         raise
684       end
685     end
686
687     # ORA-00028: your session has been killed
688     # ORA-01012: not logged on
689     # ORA-03113: end-of-file on communication channel
690     # ORA-03114: not connected to ORACLE
691     LOST_CONNECTION_ERROR_CODES = [ 28, 1012, 3113, 3114 ]
692
693     # Adds auto-recovery functionality.
694     #
695     # See: http://www.jiubao.org/ruby-oci8/api.en.html#label-11
696     def exec(sql, *bindvars, &block)
697       should_retry = self.class.auto_retry? && autocommit?
698
699       begin
700         @connection.exec(sql, *bindvars, &block)
701       rescue OCIException => e
702         raise unless LOST_CONNECTION_ERROR_CODES.include?(e.code)
703         @active = false
704         raise unless should_retry
705         should_retry = false
706         reset! rescue nil
707         retry
708       end
709     end
710
711   end
712
713 rescue LoadError
714   # OCI8 driver is unavailable.
715   module ActiveRecord # :nodoc:
716     class Base
717       @@oracle_error_message = "Oracle/OCI libraries could not be loaded: #{$!.to_s}"
718       def self.oracle_connection(config) # :nodoc:
719         # Set up a reasonable error message
720         raise LoadError, @@oracle_error_message
721       end
722       def self.oci_connection(config) # :nodoc:
723         # Set up a reasonable error message
724         raise LoadError, @@oracle_error_message
725       end
726     end
727   end
728 end
Note: See TracBrowser for help on using the browser.