activerecord-oracle_enhanced-adapter 1.1.0

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.

Potentially problematic release.


This version of activerecord-oracle_enhanced-adapter might be problematic. Click here for more details.

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