activerecord-oracle_enhanced-adapter 1.1.0

Sign up to get free protection for your applications and to get access to all the features.

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