rwc9u-oracle-enhanced 1.1.9.4

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,89 @@
1
+ == 1.1.9 2009-01-02
2
+
3
+ * Enhancements
4
+ * Added support for table and column comments in migrations
5
+ * Added support for specifying sequence start values
6
+ * Added :privilege option (e.g. :SYSDBA) to ActiveRecord::Base.establish_connection
7
+ * Bug fixes:
8
+ * Do not mark empty decimals, strings and texts (stored as NULL in database) as changed when reassigning them (starting from Rails 2.1)
9
+ * Create booleans as VARCHAR2(1) columns if emulate_booleans_from_strings is true
10
+
11
+ == 1.1.8 2008-10-10
12
+
13
+ * Bug fixes:
14
+ * Fixed storing of serialized LOB columns
15
+ * Prevent from SQL injection in :limit and :offset
16
+ * Order by LOB columns (by replacing column with function which returns first 100 characters of LOB)
17
+ * Sequence creation for tables with non-default primary key in create_table block
18
+ * Do count distinct workaround only when composite_primary_keys gem is used
19
+ (otherwise count distinct did not work with ActiveRecord 2.1.1)
20
+ * Fixed rake db:test:clone_structure task
21
+ (see http://rsim.lighthouseapp.com/projects/11468/tickets/11-rake-dbtestclone_structure-fails-in-117)
22
+ * Fixed bug when ActiveRecord::Base.allow_concurrency = true
23
+ (see http://dev.rubyonrails.org/ticket/11134)
24
+
25
+ == 1.1.7 2008-08-20
26
+
27
+ * Bug fixes:
28
+ * Fixed that adapter works without ruby-plsql gem (in this case just custom create/update/delete methods are not available)
29
+
30
+ == 1.1.6 2008-08-19
31
+
32
+ * Enhancements:
33
+ * Added support for set_date_columns and set_datetime_columns
34
+ * Added support for set_boolean_columns
35
+ * Added support for schema prefix in set_table_name (removed table name quoting)
36
+ * Added support for NVARCHAR2 column type
37
+ * Bug fixes:
38
+ * Do not call write_lobs callback when custom create or update methods are defined
39
+
40
+ == 1.1.5 2008-07-27
41
+
42
+ * Bug fixes:
43
+ * Fixed that write_lobs callback works with partial_updates enabled (added additional record lock before writing BLOB data to database)
44
+ * Enhancements:
45
+ * Changed SQL SELECT in indexes method so that it will execute faster on some large data dictionaries
46
+ * Support for other date and time formats when assigning string to :date or :datetime column
47
+
48
+ == 1.1.4 2008-07-14
49
+
50
+ * Enhancements:
51
+ * Date/Time quoting changes to support composite_primary_keys
52
+ * Added additional methods that are used by composite_primary_keys
53
+
54
+ == 1.1.3 2008-07-10
55
+
56
+ * Enhancements:
57
+ * Added support for custom create, update and delete methods when working with legacy databases where
58
+ PL/SQL API should be used for create, update and delete operations
59
+
60
+ == 1.1.2 2008-07-08
61
+
62
+ * Bug fixes:
63
+ * Fixed after_save callback addition for session store in ActiveRecord version 2.0.2
64
+ * Changed date column name recognition - now should match regex /(^|_)date(_|$)/i
65
+ (previously "updated_at" was recognized as :date column and not as :datetime)
66
+
67
+ == 1.1.1 2008-06-28
68
+
69
+ * Enhancements:
70
+ * Added ignore_table_columns option
71
+ * Added support for TIMESTAMP columns (without fractional seconds)
72
+ * NLS_DATE_FORMAT and NLS_TIMESTAMP_FORMAT independent DATE and TIMESTAMP columns support
73
+ * Bug fixes:
74
+ * Checks if CGI::Session::ActiveRecordStore::Session does not have enhanced_write_lobs callback before adding it
75
+ (Rails 2.0 does not add this callback, Rails 2.1 does)
76
+
77
+ == 1.1.0 2008-05-05
78
+
79
+ * Forked from original activerecord-oracle-adapter-1.0.0.9216
80
+ * Renamed oracle adapter to oracle_enhanced adapter
81
+ * Added "enhanced" to method and class definitions so that oracle_enhanced and original oracle adapter
82
+ could be used simultaniously
83
+ * Added Rails rake tasks as a copy from original oracle tasks
84
+ * Enhancements:
85
+ * Improved perfomance of schema dump methods when used on large data dictionaries
86
+ * Added LOB writing callback for sessions stored in database
87
+ * Added emulate_dates_by_column_name option
88
+ * Added emulate_integers_by_column_name option
89
+ * Added emulate_booleans_from_strings option
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/Manifest.txt ADDED
@@ -0,0 +1,23 @@
1
+ #oracle-enhanced.gemspec#
2
+ History.txt
3
+ License.txt
4
+ Manifest.txt
5
+ README.txt
6
+ lib/active_record/connection_adapters/emulation/oracle_adapter.rb
7
+ lib/active_record/connection_adapters/oracle_enhanced.rake
8
+ lib/active_record/connection_adapters/oracle_enhanced_adapter.rb
9
+ lib/active_record/connection_adapters/oracle_enhanced_cpk.rb
10
+ lib/active_record/connection_adapters/oracle_enhanced_dirty.rb
11
+ lib/active_record/connection_adapters/oracle_enhanced_procedures.rb
12
+ lib/active_record/connection_adapters/oracle_enhanced_reserved_words.rb
13
+ lib/active_record/connection_adapters/oracle_enhanced_tasks.rb
14
+ lib/active_record/connection_adapters/oracle_enhanced_version.rb
15
+ oracle-enhanced.gemspec
16
+ spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb
17
+ spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb
18
+ spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb
19
+ spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb
20
+ spec/active_record/connection_adapters/oracle_enhanced_emulate_oracle_adapter_spec.rb
21
+ spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb
22
+ spec/spec.opts
23
+ spec/spec_helper.rb
data/README.txt ADDED
@@ -0,0 +1,51 @@
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://github.com/rsim/oracle-enhanced/wikis for more information.
11
+
12
+ For questions and feature discussion please use http://groups.google.com/group/oracle-enhanced
13
+
14
+ Blog posts about oracle-enahnced can be found at http://blog.rayapps.com/category/oracle-enhanced
15
+
16
+ Bugs and enhancement requests can be reported at http://rsim.lighthouseapp.com/projects/11468-oracle-enhanced
17
+
18
+ == REQUIREMENTS:
19
+
20
+ * Works (has been tested) with ActiveRecord version 2.0, 2.1 and 2.2 (these are the same as Rails versions)
21
+ * Requires ruby-oci8 library to connect to Oracle
22
+ * Requires ruby-plsql gem to support custom create, update and delete methods (but can be used without ruby-plsql if this functionality is not needed)
23
+
24
+ == INSTALL:
25
+
26
+ * sudo gem install activerecord-oracle_enhanced-adapter
27
+
28
+ == LICENSE:
29
+
30
+ (The MIT License)
31
+
32
+ Copyright (c) 2009 Graham Jenkins, Michael Schoen, Raimonds Simanovskis
33
+
34
+ Permission is hereby granted, free of charge, to any person obtaining
35
+ a copy of this software and associated documentation files (the
36
+ 'Software'), to deal in the Software without restriction, including
37
+ without limitation the rights to use, copy, modify, merge, publish,
38
+ distribute, sublicense, and/or sell copies of the Software, and to
39
+ permit persons to whom the Software is furnished to do so, subject to
40
+ the following conditions:
41
+
42
+ The above copyright notice and this permission notice shall be
43
+ included in all copies or substantial portions of the Software.
44
+
45
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
46
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
47
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
48
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
49
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
50
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
51
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,5 @@
1
+ class ActiveRecord::ConnectionAdapters::OracleAdapter < ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter
2
+ def adapter_name
3
+ 'Oracle'
4
+ end
5
+ end
@@ -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.chop)
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.chop)
40
+ end
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,1137 @@
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
+ # RSI: added mapping for TIMESTAMP / WITH TIME ZONE / LOCAL TIME ZONE types
43
+ # currently Ruby-OCI8 does not support fractional seconds for timestamps
44
+ OCI8::BindType::Mapping[OCI8::SQLT_TIMESTAMP] = OCI8::BindType::OraDate
45
+ OCI8::BindType::Mapping[OCI8::SQLT_TIMESTAMP_TZ] = OCI8::BindType::OraDate
46
+ OCI8::BindType::Mapping[OCI8::SQLT_TIMESTAMP_LTZ] = OCI8::BindType::OraDate
47
+
48
+ module ActiveRecord
49
+ class Base
50
+ def self.oracle_enhanced_connection(config) #:nodoc:
51
+ # Use OCI8AutoRecover instead of normal OCI8 driver.
52
+ if config[:emulate_oracle_adapter] == true
53
+ # allows the enhanced adapter to look like the OracleAdapter. Useful to pick up
54
+ # conditionals in the rails activerecord test suite
55
+ require 'active_record/connection_adapters/emulation/oracle_adapter'
56
+ ConnectionAdapters::OracleAdapter.new OCI8EnhancedAutoRecover.new(config), logger
57
+ else
58
+ ConnectionAdapters::OracleEnhancedAdapter.new OCI8EnhancedAutoRecover.new(config), logger
59
+ end
60
+ end
61
+
62
+ # RSI: specify table columns which should be ifnored
63
+ def self.ignore_table_columns(*args)
64
+ connection.ignore_table_columns(table_name,*args)
65
+ end
66
+
67
+ # RSI: specify which table columns should be treated as date (without time)
68
+ def self.set_date_columns(*args)
69
+ connection.set_type_for_columns(table_name,:date,*args)
70
+ end
71
+
72
+ # RSI: specify which table columns should be treated as datetime
73
+ def self.set_datetime_columns(*args)
74
+ connection.set_type_for_columns(table_name,:datetime,*args)
75
+ end
76
+
77
+ # RSI: specify which table columns should be treated as booleans
78
+ def self.set_boolean_columns(*args)
79
+ connection.set_type_for_columns(table_name,:boolean,*args)
80
+ end
81
+
82
+ # After setting large objects to empty, select the OCI8::LOB
83
+ # and write back the data.
84
+ after_save :enhanced_write_lobs
85
+ def enhanced_write_lobs #:nodoc:
86
+ if connection.is_a?(ConnectionAdapters::OracleEnhancedAdapter) &&
87
+ !(self.class.custom_create_method || self.class.custom_update_method)
88
+ connection.write_lobs(self.class.table_name, self.class, attributes)
89
+ end
90
+ end
91
+ private :enhanced_write_lobs
92
+
93
+ class << self
94
+ # RSI: patch ORDER BY to work with LOBs
95
+ def add_order_with_lobs!(sql, order, scope = :auto)
96
+ if connection.is_a?(ConnectionAdapters::OracleEnhancedAdapter)
97
+ order = connection.lob_order_by_expression(self, order) if order
98
+
99
+ orig_scope = scope
100
+ scope = scope(:find) if :auto == scope
101
+ if scope
102
+ new_scope_order = connection.lob_order_by_expression(self, scope[:order])
103
+ if new_scope_order != scope[:order]
104
+ scope = scope.merge(:order => new_scope_order)
105
+ else
106
+ scope = orig_scope
107
+ end
108
+ end
109
+ end
110
+ add_order_without_lobs!(sql, order, scope = :auto)
111
+ end
112
+ private :add_order_with_lobs!
113
+ alias_method :add_order_without_lobs!, :add_order!
114
+ alias_method :add_order!, :add_order_with_lobs!
115
+ end
116
+
117
+ # RSI: get table comment from schema definition
118
+ def self.table_comment
119
+ self.connection.table_comment(self.table_name)
120
+ end
121
+ end
122
+
123
+
124
+ module ConnectionAdapters #:nodoc:
125
+ class OracleEnhancedColumn < Column #:nodoc:
126
+
127
+ attr_reader :table_name, :forced_column_type
128
+
129
+ def initialize(name, default, sql_type = nil, null = true, table_name = nil, forced_column_type = nil)
130
+ @table_name = table_name
131
+ @forced_column_type = forced_column_type
132
+ super(name, default, sql_type, null)
133
+ end
134
+
135
+ def type_cast(value)
136
+ return guess_date_or_time(value) if type == :datetime && OracleEnhancedAdapter.emulate_dates
137
+ super
138
+ end
139
+
140
+ # convert something to a boolean
141
+ # RSI: added y as boolean value
142
+ def self.value_to_boolean(value)
143
+ if value == true || value == false
144
+ value
145
+ else
146
+ %w(true t 1 y +).include?(value.to_s.downcase)
147
+ end
148
+ end
149
+
150
+ # RSI: convert Time value to Date for :date columns
151
+ def self.string_to_date(string)
152
+ return string.to_date if string.is_a?(Time)
153
+ super
154
+ end
155
+
156
+ # RSI: convert Date value to Time for :datetime columns
157
+ def self.string_to_time(string)
158
+ return string.to_time if string.is_a?(Date) && !OracleEnhancedAdapter.emulate_dates
159
+ super
160
+ end
161
+
162
+ # RSI: get column comment from schema definition
163
+ # will work only if using default ActiveRecord connection
164
+ def comment
165
+ ActiveRecord::Base.connection.column_comment(@table_name, name)
166
+ end
167
+
168
+ private
169
+ def simplified_type(field_type)
170
+ return :boolean if OracleEnhancedAdapter.emulate_booleans && field_type == 'NUMBER(1)'
171
+ return :boolean if OracleEnhancedAdapter.emulate_booleans_from_strings &&
172
+ (forced_column_type == :boolean ||
173
+ OracleEnhancedAdapter.is_boolean_column?(name, field_type, table_name))
174
+
175
+ case field_type
176
+ when /date/i
177
+ forced_column_type ||
178
+ (:date if OracleEnhancedAdapter.emulate_dates_by_column_name && OracleEnhancedAdapter.is_date_column?(name, table_name)) ||
179
+ :datetime
180
+ when /timestamp/i then :timestamp
181
+ when /time/i then :datetime
182
+ when /decimal|numeric|number/i
183
+ return :integer if extract_scale(field_type) == 0
184
+ # RSI: if column name is ID or ends with _ID
185
+ return :integer if OracleEnhancedAdapter.emulate_integers_by_column_name && OracleEnhancedAdapter.is_integer_column?(name, table_name)
186
+ :decimal
187
+ else super
188
+ end
189
+ end
190
+
191
+ def guess_date_or_time(value)
192
+ value.respond_to?(:hour) && (value.hour == 0 and value.min == 0 and value.sec == 0) ?
193
+ Date.new(value.year, value.month, value.day) : value
194
+ end
195
+
196
+ class << self
197
+ protected
198
+
199
+ def fallback_string_to_date(string)
200
+ if OracleEnhancedAdapter.string_to_date_format || OracleEnhancedAdapter.string_to_time_format
201
+ return (string_to_date_or_time_using_format(string).to_date rescue super)
202
+ end
203
+ super
204
+ end
205
+
206
+ def fallback_string_to_time(string)
207
+ if OracleEnhancedAdapter.string_to_time_format || OracleEnhancedAdapter.string_to_date_format
208
+ return (string_to_date_or_time_using_format(string).to_time rescue super)
209
+ end
210
+ super
211
+ end
212
+
213
+ def string_to_date_or_time_using_format(string)
214
+ if OracleEnhancedAdapter.string_to_time_format && dt=Date._strptime(string, OracleEnhancedAdapter.string_to_time_format)
215
+ return Time.mktime(*dt.values_at(:year, :mon, :mday, :hour, :min, :sec, :zone, :wday))
216
+ end
217
+ DateTime.strptime(string, OracleEnhancedAdapter.string_to_date_format)
218
+ end
219
+
220
+ end
221
+ end
222
+
223
+
224
+ # This is an Oracle/OCI adapter for the ActiveRecord persistence
225
+ # framework. It relies upon the OCI8 driver, which works with Oracle 8i
226
+ # and above. Most recent development has been on Debian Linux against
227
+ # a 10g database, ActiveRecord 1.12.1 and OCI8 0.1.13.
228
+ # See: http://rubyforge.org/projects/ruby-oci8/
229
+ #
230
+ # Usage notes:
231
+ # * Key generation assumes a "${table_name}_seq" sequence is available
232
+ # for all tables; the sequence name can be changed using
233
+ # ActiveRecord::Base.set_sequence_name. When using Migrations, these
234
+ # sequences are created automatically.
235
+ # * Oracle uses DATE or TIMESTAMP datatypes for both dates and times.
236
+ # Consequently some hacks are employed to map data back to Date or Time
237
+ # in Ruby. If the column_name ends in _time it's created as a Ruby Time.
238
+ # Else if the hours/minutes/seconds are 0, I make it a Ruby Date. Else
239
+ # it's a Ruby Time. This is a bit nasty - but if you use Duck Typing
240
+ # you'll probably not care very much. In 9i and up it's tempting to
241
+ # map DATE to Date and TIMESTAMP to Time, but too many databases use
242
+ # DATE for both. Timezones and sub-second precision on timestamps are
243
+ # not supported.
244
+ # * Default values that are functions (such as "SYSDATE") are not
245
+ # supported. This is a restriction of the way ActiveRecord supports
246
+ # default values.
247
+ # * Support for Oracle8 is limited by Rails' use of ANSI join syntax, which
248
+ # is supported in Oracle9i and later. You will need to use #finder_sql for
249
+ # has_and_belongs_to_many associations to run against Oracle8.
250
+ #
251
+ # Required parameters:
252
+ #
253
+ # * <tt>:username</tt>
254
+ # * <tt>:password</tt>
255
+ # * <tt>:database</tt>
256
+ class OracleEnhancedAdapter < AbstractAdapter
257
+
258
+ @@emulate_booleans = true
259
+ cattr_accessor :emulate_booleans
260
+
261
+ @@emulate_dates = false
262
+ cattr_accessor :emulate_dates
263
+
264
+ # RSI: set to true if columns with DATE in their name should be emulated as date
265
+ @@emulate_dates_by_column_name = false
266
+ cattr_accessor :emulate_dates_by_column_name
267
+ def self.is_date_column?(name, table_name = nil)
268
+ name =~ /(^|_)date(_|$)/i
269
+ end
270
+ # RSI: instance method uses at first check if column type defined at class level
271
+ def is_date_column?(name, table_name = nil)
272
+ case get_type_for_column(table_name, name)
273
+ when nil
274
+ self.class.is_date_column?(name, table_name)
275
+ when :date
276
+ true
277
+ else
278
+ false
279
+ end
280
+ end
281
+
282
+ # RSI: set to true if NUMBER columns with ID at the end of their name should be emulated as integers
283
+ @@emulate_integers_by_column_name = false
284
+ cattr_accessor :emulate_integers_by_column_name
285
+ def self.is_integer_column?(name, table_name = nil)
286
+ name =~ /(^|_)id$/i
287
+ end
288
+
289
+ # RSI: set to true if CHAR(1), VARCHAR2(1) columns or VARCHAR2 columns with FLAG or YN at the end of their name
290
+ # should be emulated as booleans
291
+ @@emulate_booleans_from_strings = false
292
+ cattr_accessor :emulate_booleans_from_strings
293
+ def self.is_boolean_column?(name, field_type, table_name = nil)
294
+ return true if ["CHAR(1)","VARCHAR2(1)"].include?(field_type)
295
+ field_type =~ /^VARCHAR2/ && (name =~ /_flag$/i || name =~ /_yn$/i)
296
+ end
297
+ def self.boolean_to_string(bool)
298
+ bool ? "Y" : "N"
299
+ end
300
+
301
+ # RSI: use to set NLS specific date formats which will be used when assigning string to :date and :datetime columns
302
+ @@string_to_date_format = @@string_to_time_format = nil
303
+ cattr_accessor :string_to_date_format, :string_to_time_format
304
+
305
+ def adapter_name #:nodoc:
306
+ 'OracleEnhanced'
307
+ end
308
+
309
+ def supports_migrations? #:nodoc:
310
+ true
311
+ end
312
+
313
+ def native_database_types #:nodoc:
314
+ {
315
+ :primary_key => "NUMBER(38) NOT NULL PRIMARY KEY",
316
+ :string => { :name => "VARCHAR2", :limit => 255 },
317
+ :text => { :name => "CLOB" },
318
+ :integer => { :name => "NUMBER", :limit => 38 },
319
+ :float => { :name => "NUMBER" },
320
+ :decimal => { :name => "DECIMAL" },
321
+ :datetime => { :name => "DATE" },
322
+ # RSI: changed to native TIMESTAMP type
323
+ # :timestamp => { :name => "DATE" },
324
+ :timestamp => { :name => "TIMESTAMP" },
325
+ :time => { :name => "DATE" },
326
+ :date => { :name => "DATE" },
327
+ :binary => { :name => "BLOB" },
328
+ # RSI: if emulate_booleans_from_strings then store booleans in VARCHAR2
329
+ :boolean => emulate_booleans_from_strings ?
330
+ { :name => "VARCHAR2", :limit => 1 } : { :name => "NUMBER", :limit => 1 }
331
+ }
332
+ end
333
+
334
+ def table_alias_length
335
+ 30
336
+ end
337
+
338
+ # Returns an array of arrays containing the field values.
339
+ # Order is the same as that returned by #columns.
340
+ def select_rows(sql, name = nil)
341
+ result = select(sql, name)
342
+ result.map{ |v| v.values}
343
+ end
344
+
345
+ # QUOTING ==================================================
346
+ #
347
+ # see: abstract/quoting.rb
348
+
349
+ # camelCase column names need to be quoted; not that anyone using Oracle
350
+ # would really do this, but handling this case means we pass the test...
351
+ def quote_column_name(name) #:nodoc:
352
+ name.to_s =~ /[A-Z]/ ? "\"#{name}\"" : quote_oracle_reserved_words(name)
353
+ end
354
+
355
+ # abstract_adapter calls quote_column_name from quote_table_name, so prevent that
356
+ # but still quote names that have non alphanumeric values
357
+ def quote_table_name(name)
358
+ if name.to_s =~ /^[A-Z_0-9\.]+$/i
359
+ name
360
+ else
361
+ "\"#{name}\""
362
+ end
363
+ end
364
+
365
+ def quote_string(s) #:nodoc:
366
+ s.gsub(/'/, "''")
367
+ end
368
+
369
+ def quote(value, column = nil) #:nodoc:
370
+ if value && column
371
+ case column.type
372
+ when :text, :binary
373
+ %Q{empty_#{ column.sql_type.downcase rescue 'blob' }()}
374
+ # RSI: TIMESTAMP support
375
+ when :timestamp
376
+ quote_timestamp_with_to_timestamp(value)
377
+ # RSI: NLS_DATE_FORMAT independent DATE support
378
+ when :date, :time, :datetime
379
+ quote_date_with_to_date(value)
380
+ else
381
+ super
382
+ end
383
+ elsif value.acts_like?(:date)
384
+ quote_date_with_to_date(value)
385
+ elsif value.acts_like?(:time)
386
+ value.to_i == value.to_f ? quote_date_with_to_date(value) : quote_timestamp_with_to_timestamp(value)
387
+ else
388
+ super
389
+ end
390
+ end
391
+
392
+ def quoted_true
393
+ return "'#{self.class.boolean_to_string(true)}'" if emulate_booleans_from_strings
394
+ "1"
395
+ end
396
+
397
+ def quoted_false
398
+ return "'#{self.class.boolean_to_string(false)}'" if emulate_booleans_from_strings
399
+ "0"
400
+ end
401
+
402
+ # RSI: should support that composite_primary_keys gem will pass date as string
403
+ def quote_date_with_to_date(value)
404
+ value = value.to_s(:db) if value.acts_like?(:date) || value.acts_like?(:time)
405
+ "TO_DATE('#{value}','YYYY-MM-DD HH24:MI:SS')"
406
+ end
407
+
408
+ def quote_timestamp_with_to_timestamp(value)
409
+ # add up to 9 digits of fractional seconds to inserted time
410
+ value = "#{value.to_s(:db)}.#{("%.6f"%value.to_f).split('.')[1]}" if value.acts_like?(:time)
411
+ "TO_TIMESTAMP('#{value}','YYYY-MM-DD HH24:MI:SS.FF6')"
412
+ end
413
+
414
+ # CONNECTION MANAGEMENT ====================================
415
+ #
416
+
417
+ # Returns true if the connection is active.
418
+ def active?
419
+ # Pings the connection to check if it's still good. Note that an
420
+ # #active? method is also available, but that simply returns the
421
+ # last known state, which isn't good enough if the connection has
422
+ # gone stale since the last use.
423
+ @connection.ping
424
+ rescue OCIException
425
+ false
426
+ end
427
+
428
+ # Reconnects to the database.
429
+ def reconnect!
430
+ @connection.reset!
431
+ rescue OCIException => e
432
+ @logger.warn "#{adapter_name} automatic reconnection failed: #{e.message}"
433
+ end
434
+
435
+ # Disconnects from the database.
436
+ def disconnect!
437
+ @connection.logoff rescue nil
438
+ @connection.active = false
439
+ end
440
+
441
+
442
+ # DATABASE STATEMENTS ======================================
443
+ #
444
+ # see: abstract/database_statements.rb
445
+
446
+ def execute(sql, name = nil) #:nodoc:
447
+ log(sql, name) { @connection.exec sql }
448
+ end
449
+
450
+ # Returns the next sequence value from a sequence generator. Not generally
451
+ # called directly; used by ActiveRecord to get the next primary key value
452
+ # when inserting a new database record (see #prefetch_primary_key?).
453
+ def next_sequence_value(sequence_name)
454
+ id = 0
455
+ @connection.exec("select #{sequence_name}.nextval id from dual") { |r| id = r[0].to_i }
456
+ id
457
+ end
458
+
459
+ def begin_db_transaction #:nodoc:
460
+ @connection.autocommit = false
461
+ end
462
+
463
+ def commit_db_transaction #:nodoc:
464
+ @connection.commit
465
+ ensure
466
+ @connection.autocommit = true
467
+ end
468
+
469
+ def rollback_db_transaction #:nodoc:
470
+ @connection.rollback
471
+ ensure
472
+ @connection.autocommit = true
473
+ end
474
+
475
+ def add_limit_offset!(sql, options) #:nodoc:
476
+ # RSI: added to_i for limit and offset to protect from SQL injection
477
+ offset = (options[:offset] || 0).to_i
478
+
479
+ if limit = options[:limit]
480
+ limit = limit.to_i
481
+ sql.replace "select * from (select raw_sql_.*, rownum raw_rnum_ from (#{sql}) raw_sql_ where rownum <= #{offset+limit}) where raw_rnum_ > #{offset}"
482
+ elsif offset > 0
483
+ sql.replace "select * from (select raw_sql_.*, rownum raw_rnum_ from (#{sql}) raw_sql_) where raw_rnum_ > #{offset}"
484
+ end
485
+ end
486
+
487
+ # Returns true for Oracle adapter (since Oracle requires primary key
488
+ # values to be pre-fetched before insert). See also #next_sequence_value.
489
+ def prefetch_primary_key?(table_name = nil)
490
+ true
491
+ end
492
+
493
+ def default_sequence_name(table, column) #:nodoc:
494
+ quote_table_name("#{table}_seq")
495
+ end
496
+
497
+
498
+ # Inserts the given fixture into the table. Overridden to properly handle lobs.
499
+ def insert_fixture(fixture, table_name)
500
+ super
501
+
502
+ klass = fixture.class_name.constantize rescue nil
503
+ if klass.respond_to?(:ancestors) && klass.ancestors.include?(ActiveRecord::Base)
504
+ write_lobs(table_name, klass, fixture)
505
+ end
506
+ end
507
+
508
+ # Writes LOB values from attributes, as indicated by the LOB columns of klass.
509
+ def write_lobs(table_name, klass, attributes)
510
+ id = quote(attributes[klass.primary_key])
511
+ klass.columns.select { |col| col.sql_type =~ /LOB$/i }.each do |col|
512
+ value = attributes[col.name]
513
+ # RSI: changed sequence of next two lines - should check if value is nil before converting to yaml
514
+ next if value.nil? || (value == '')
515
+ value = value.to_yaml if col.text? && klass.serialized_attributes[col.name]
516
+ uncached do
517
+ lob = select_one("SELECT #{col.name} FROM #{table_name} WHERE #{klass.primary_key} = #{id} FOR UPDATE",
518
+ 'Writable Large Object')[col.name]
519
+ lob.write value
520
+ end
521
+ end
522
+ end
523
+
524
+ # RSI: change LOB column for ORDER BY clause
525
+ # just first 100 characters are taken for ordering
526
+ def lob_order_by_expression(klass, order)
527
+ return order if order.nil?
528
+ changed = false
529
+ new_order = order.to_s.strip.split(/, */).map do |order_by_col|
530
+ column_name, asc_desc = order_by_col.split(/ +/)
531
+ if column = klass.columns.detect { |col| col.name == column_name && col.sql_type =~ /LOB$/i}
532
+ changed = true
533
+ "DBMS_LOB.SUBSTR(#{column_name},100,1) #{asc_desc}"
534
+ else
535
+ order_by_col
536
+ end
537
+ end.join(', ')
538
+ changed ? new_order : order
539
+ end
540
+
541
+ # SCHEMA STATEMENTS ========================================
542
+ #
543
+ # see: abstract/schema_statements.rb
544
+
545
+ def current_database #:nodoc:
546
+ select_one("select sys_context('userenv','db_name') db from dual")["db"]
547
+ end
548
+
549
+ # RSI: changed select from user_tables to all_tables - much faster in large data dictionaries
550
+ def tables(name = nil) #:nodoc:
551
+ select_all("select lower(table_name) from all_tables where owner = sys_context('userenv','session_user')").inject([]) do | tabs, t |
552
+ tabs << t.to_a.first.last
553
+ end
554
+ end
555
+
556
+ def indexes(table_name, name = nil) #:nodoc:
557
+ result = select_all(<<-SQL, name)
558
+ SELECT lower(i.index_name) as index_name, i.uniqueness, lower(c.column_name) as column_name
559
+ FROM all_indexes i, user_ind_columns c
560
+ WHERE i.table_name = '#{table_name.to_s.upcase}'
561
+ AND c.index_name = i.index_name
562
+ AND i.index_name NOT IN (SELECT uc.index_name FROM user_constraints uc WHERE uc.constraint_type = 'P')
563
+ AND i.owner = sys_context('userenv','session_user')
564
+ ORDER BY i.index_name, c.column_position
565
+ SQL
566
+
567
+ current_index = nil
568
+ indexes = []
569
+
570
+ result.each do |row|
571
+ if current_index != row['index_name']
572
+ indexes << IndexDefinition.new(table_name, row['index_name'], row['uniqueness'] == "UNIQUE", [])
573
+ current_index = row['index_name']
574
+ end
575
+
576
+ indexes.last.columns << row['column_name']
577
+ end
578
+
579
+ indexes
580
+ end
581
+
582
+ # RSI: set ignored columns for table
583
+ def ignore_table_columns(table_name, *args)
584
+ @ignore_table_columns ||= {}
585
+ @ignore_table_columns[table_name] ||= []
586
+ @ignore_table_columns[table_name] += args.map{|a| a.to_s.downcase}
587
+ @ignore_table_columns[table_name].uniq!
588
+ end
589
+
590
+ def ignored_table_columns(table_name)
591
+ @ignore_table_columns ||= {}
592
+ @ignore_table_columns[table_name]
593
+ end
594
+
595
+ # RSI: set explicit type for specified table columns
596
+ def set_type_for_columns(table_name, column_type, *args)
597
+ @table_column_type ||= {}
598
+ @table_column_type[table_name] ||= {}
599
+ args.each do |col|
600
+ @table_column_type[table_name][col.to_s.downcase] = column_type
601
+ end
602
+ end
603
+
604
+ def get_type_for_column(table_name, column_name)
605
+ result = @table_column_type && @table_column_type[table_name] && @table_column_type[table_name][column_name.to_s.downcase]
606
+ result
607
+ end
608
+
609
+ def clear_types_for_columns
610
+ @table_column_type = nil
611
+ end
612
+
613
+ def columns(table_name, name = nil) #:nodoc:
614
+ # RSI: get ignored_columns by original table name
615
+ ignored_columns = ignored_table_columns(table_name)
616
+
617
+ (owner, desc_table_name) = @connection.describe(quote_table_name(table_name))
618
+
619
+ table_cols = <<-SQL
620
+ select column_name as name, data_type as sql_type, data_default, nullable,
621
+ decode(data_type, 'NUMBER', data_precision,
622
+ 'FLOAT', data_precision,
623
+ 'VARCHAR2', data_length,
624
+ 'CHAR', data_length,
625
+ null) as limit,
626
+ decode(data_type, 'NUMBER', data_scale, null) as scale
627
+ from all_tab_columns
628
+ where owner = '#{owner}'
629
+ and table_name = '#{desc_table_name}'
630
+ order by column_id
631
+ SQL
632
+
633
+ # RSI: added deletion of ignored columns
634
+ select_all(table_cols, name).delete_if do |row|
635
+ ignored_columns && ignored_columns.include?(row['name'].downcase)
636
+ end.map do |row|
637
+ limit, scale = row['limit'], row['scale']
638
+ if limit || scale
639
+ row['sql_type'] << "(#{(limit || 38).to_i}" + ((scale = scale.to_i) > 0 ? ",#{scale})" : ")")
640
+ end
641
+
642
+ # clean up odd default spacing from Oracle
643
+ if row['data_default']
644
+ row['data_default'].sub!(/^(.*?)\s*$/, '\1')
645
+ row['data_default'].sub!(/^'(.*)'$/, '\1')
646
+ row['data_default'] = nil if row['data_default'] =~ /^(null|empty_[bc]lob\(\))$/i
647
+ end
648
+
649
+ OracleEnhancedColumn.new(oracle_downcase(row['name']),
650
+ row['data_default'],
651
+ row['sql_type'],
652
+ row['nullable'] == 'Y',
653
+ # RSI: pass table name for table specific column definitions
654
+ table_name,
655
+ # RSI: pass column type if specified in class definition
656
+ get_type_for_column(table_name, oracle_downcase(row['name'])))
657
+ end
658
+ end
659
+
660
+ # RSI: default sequence start with value
661
+ @@default_sequence_start_value = 10000
662
+ cattr_accessor :default_sequence_start_value
663
+
664
+ def create_table(name, options = {}, &block) #:nodoc:
665
+ create_sequence = options[:id] != false
666
+ column_comments = {}
667
+ super(name, options) do |t|
668
+ # store that primary key was defined in create_table block
669
+ unless create_sequence
670
+ class << t
671
+ attr_accessor :create_sequence
672
+ def primary_key(*args)
673
+ self.create_sequence = true
674
+ super(*args)
675
+ end
676
+ end
677
+ end
678
+
679
+ # store column comments
680
+ class << t
681
+ attr_accessor :column_comments
682
+ def column(name, type, options = {})
683
+ if options[:comment]
684
+ self.column_comments ||= {}
685
+ self.column_comments[name] = options[:comment]
686
+ end
687
+ super(name, type, options)
688
+ end
689
+ end
690
+
691
+ result = block.call(t)
692
+ create_sequence = create_sequence || t.create_sequence
693
+ column_comments = t.column_comments if t.column_comments
694
+ end
695
+
696
+ seq_name = options[:sequence_name] || quote_table_name("#{name}_seq")
697
+ seq_start_value = options[:sequence_start_value] || default_sequence_start_value
698
+ execute "CREATE SEQUENCE #{seq_name} START WITH #{seq_start_value}" if create_sequence
699
+
700
+ add_table_comment name, options[:comment]
701
+ column_comments.each do |column_name, comment|
702
+ add_comment name, column_name, comment
703
+ end
704
+
705
+ end
706
+
707
+ def rename_table(name, new_name) #:nodoc:
708
+ execute "RENAME #{name} TO #{new_name}"
709
+ execute "RENAME #{name}_seq TO #{new_name}_seq" rescue nil
710
+ end
711
+
712
+ def drop_table(name, options = {}) #:nodoc:
713
+ super(name)
714
+ seq_name = options[:sequence_name] || quote_table_name("#{name}_seq")
715
+ execute "DROP SEQUENCE #{seq_name}" rescue nil
716
+ end
717
+
718
+ def remove_index(table_name, options = {}) #:nodoc:
719
+ execute "DROP INDEX #{index_name(table_name, options)}"
720
+ end
721
+
722
+ def change_column_default(table_name, column_name, default) #:nodoc:
723
+ execute "ALTER TABLE #{table_name} MODIFY #{quote_column_name(column_name)} DEFAULT #{quote(default)}"
724
+ end
725
+
726
+ def change_column(table_name, column_name, type, options = {}) #:nodoc:
727
+ change_column_sql = "ALTER TABLE #{table_name} MODIFY #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
728
+ add_column_options!(change_column_sql, options)
729
+ execute(change_column_sql)
730
+ end
731
+
732
+ def rename_column(table_name, column_name, new_column_name) #:nodoc:
733
+ execute "ALTER TABLE #{table_name} RENAME COLUMN #{quote_column_name(column_name)} to #{quote_column_name(new_column_name)}"
734
+ end
735
+
736
+ def remove_column(table_name, column_name) #:nodoc:
737
+ execute "ALTER TABLE #{table_name} DROP COLUMN #{quote_column_name(column_name)}"
738
+ end
739
+
740
+ # RSI: table and column comments
741
+ def add_comment(table_name, column_name, comment)
742
+ return if comment.blank?
743
+ execute "COMMENT ON COLUMN #{quote_table_name(table_name)}.#{column_name} IS '#{comment}'"
744
+ end
745
+
746
+ def add_table_comment(table_name, comment)
747
+ return if comment.blank?
748
+ execute "COMMENT ON TABLE #{quote_table_name(table_name)} IS '#{comment}'"
749
+ end
750
+
751
+ def table_comment(table_name)
752
+ (owner, table_name) = @connection.describe(quote_table_name(table_name))
753
+ select_value <<-SQL
754
+ SELECT comments FROM all_tab_comments
755
+ WHERE owner = '#{owner}'
756
+ AND table_name = '#{table_name}'
757
+ SQL
758
+ end
759
+
760
+ def column_comment(table_name, column_name)
761
+ (owner, table_name) = @connection.describe(quote_table_name(table_name))
762
+ select_value <<-SQL
763
+ SELECT comments FROM all_col_comments
764
+ WHERE owner = '#{owner}'
765
+ AND table_name = '#{table_name}'
766
+ AND column_name = '#{column_name.upcase}'
767
+ SQL
768
+ end
769
+
770
+ # Find a table's primary key and sequence.
771
+ # *Note*: Only primary key is implemented - sequence will be nil.
772
+ def pk_and_sequence_for(table_name)
773
+ (owner, table_name) = @connection.describe(quote_table_name(table_name))
774
+
775
+ # RSI: changed select from all_constraints to user_constraints - much faster in large data dictionaries
776
+ pks = select_values(<<-SQL, 'Primary Key')
777
+ select cc.column_name
778
+ from user_constraints c, all_cons_columns cc
779
+ where c.owner = '#{owner}'
780
+ and c.table_name = '#{table_name}'
781
+ and c.constraint_type = 'P'
782
+ and cc.owner = c.owner
783
+ and cc.constraint_name = c.constraint_name
784
+ SQL
785
+
786
+ # only support single column keys
787
+ pks.size == 1 ? [oracle_downcase(pks.first), nil] : nil
788
+ end
789
+
790
+ def structure_dump #:nodoc:
791
+ s = select_all("select sequence_name from user_sequences").inject("") do |structure, seq|
792
+ structure << "create sequence #{seq.to_a.first.last};\n\n"
793
+ end
794
+
795
+ # RSI: changed select from user_tables to all_tables - much faster in large data dictionaries
796
+ select_all("select table_name from all_tables where owner = sys_context('userenv','session_user')").inject(s) do |structure, table|
797
+ ddl = "create table #{table.to_a.first.last} (\n "
798
+ cols = select_all(%Q{
799
+ select column_name, data_type, data_length, char_used, char_length, data_precision, data_scale, data_default, nullable
800
+ from user_tab_columns
801
+ where table_name = '#{table.to_a.first.last}'
802
+ order by column_id
803
+ }).map do |row|
804
+ col = "#{row['column_name'].downcase} #{row['data_type'].downcase}"
805
+ if row['data_type'] =='NUMBER' and !row['data_precision'].nil?
806
+ col << "(#{row['data_precision'].to_i}"
807
+ col << ",#{row['data_scale'].to_i}" if !row['data_scale'].nil?
808
+ col << ')'
809
+ elsif row['data_type'].include?('CHAR')
810
+ length = row['char_used'] == 'C' ? row['char_length'].to_i : row['data_length'].to_i
811
+ col << "(#{length})"
812
+ end
813
+ col << " default #{row['data_default']}" if !row['data_default'].nil?
814
+ col << ' not null' if row['nullable'] == 'N'
815
+ col
816
+ end
817
+ ddl << cols.join(",\n ")
818
+ ddl << ");\n\n"
819
+ structure << ddl
820
+ end
821
+ end
822
+
823
+ def structure_drop #:nodoc:
824
+ s = select_all("select sequence_name from user_sequences").inject("") do |drop, seq|
825
+ drop << "drop sequence #{seq.to_a.first.last};\n\n"
826
+ end
827
+
828
+ # RSI: changed select from user_tables to all_tables - much faster in large data dictionaries
829
+ select_all("select table_name from all_tables where owner = sys_context('userenv','session_user')").inject(s) do |drop, table|
830
+ drop << "drop table #{table.to_a.first.last} cascade constraints;\n\n"
831
+ end
832
+ end
833
+
834
+ def add_column_options!(sql, options) #:nodoc:
835
+ # handle case of defaults for CLOB columns, which would otherwise get "quoted" incorrectly
836
+ if options_include_default?(options) && (column = options[:column]) && column.type == :text
837
+ sql << " DEFAULT #{quote(options.delete(:default))}"
838
+ end
839
+ super
840
+ end
841
+
842
+ # SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
843
+ #
844
+ # Oracle requires the ORDER BY columns to be in the SELECT list for DISTINCT
845
+ # queries. However, with those columns included in the SELECT DISTINCT list, you
846
+ # won't actually get a distinct list of the column you want (presuming the column
847
+ # has duplicates with multiple values for the ordered-by columns. So we use the
848
+ # FIRST_VALUE function to get a single (first) value for each column, effectively
849
+ # making every row the same.
850
+ #
851
+ # distinct("posts.id", "posts.created_at desc")
852
+ def distinct(columns, order_by)
853
+ return "DISTINCT #{columns}" if order_by.blank?
854
+
855
+ # construct a valid DISTINCT clause, ie. one that includes the ORDER BY columns, using
856
+ # FIRST_VALUE such that the inclusion of these columns doesn't invalidate the DISTINCT
857
+ order_columns = order_by.split(',').map { |s| s.strip }.reject(&:blank?)
858
+ order_columns = order_columns.zip((0...order_columns.size).to_a).map do |c, i|
859
+ "FIRST_VALUE(#{c.split.first}) OVER (PARTITION BY #{columns} ORDER BY #{c}) AS alias_#{i}__"
860
+ end
861
+ sql = "DISTINCT #{columns}, "
862
+ sql << order_columns * ", "
863
+ end
864
+
865
+ # ORDER BY clause for the passed order option.
866
+ #
867
+ # Uses column aliases as defined by #distinct.
868
+ def add_order_by_for_association_limiting!(sql, options)
869
+ return sql if options[:order].blank?
870
+
871
+ order = options[:order].split(',').collect { |s| s.strip }.reject(&:blank?)
872
+ order.map! {|s| $1 if s =~ / (.*)/}
873
+ order = order.zip((0...order.size).to_a).map { |s,i| "alias_#{i}__ #{s}" }.join(', ')
874
+
875
+ sql << " ORDER BY #{order}"
876
+ end
877
+
878
+ private
879
+
880
+ def select(sql, name = nil)
881
+ cursor = execute(sql, name)
882
+ cols = cursor.get_col_names.map { |x| oracle_downcase(x) }
883
+ rows = []
884
+
885
+ while row = cursor.fetch
886
+ hash = Hash.new
887
+
888
+ cols.each_with_index do |col, i|
889
+ hash[col] =
890
+ case row[i]
891
+ when OCI8::LOB
892
+ name == 'Writable Large Object' ? row[i]: row[i].read
893
+ when OraDate
894
+ d = row[i]
895
+ # RSI: added emulate_dates_by_column_name functionality
896
+ # if emulate_dates_by_column_name && self.class.is_date_column?(col)
897
+ # d.to_date
898
+ # elsif
899
+ if emulate_dates && (d.hour == 0 && d.minute == 0 && d.second == 0)
900
+ d.to_date
901
+ else
902
+ # see string_to_time; Time overflowing to DateTime, respecting the default timezone
903
+ time_array = [d.year, d.month, d.day, d.hour, d.minute, d.second]
904
+ begin
905
+ Time.send(Base.default_timezone, *time_array)
906
+ rescue
907
+ zone_offset = if Base.default_timezone == :local then DateTime.now.offset else 0 end
908
+ # Append zero calendar reform start to account for dates skipped by calendar reform
909
+ DateTime.new(*time_array[0..5] << zone_offset << 0) rescue nil
910
+ end
911
+ end
912
+ # RSI: added emulate_integers_by_column_name functionality
913
+ when Float
914
+ n = row[i]
915
+ if emulate_integers_by_column_name && self.class.is_integer_column?(col)
916
+ n.to_i
917
+ else
918
+ n
919
+ end
920
+ else row[i]
921
+ end unless col == 'raw_rnum_'
922
+ end
923
+
924
+ rows << hash
925
+ end
926
+
927
+ rows
928
+ ensure
929
+ cursor.close if cursor
930
+ end
931
+
932
+ # Oracle column names by default are case-insensitive, but treated as upcase;
933
+ # for neatness, we'll downcase within Rails. EXCEPT that folks CAN quote
934
+ # their column names when creating Oracle tables, which makes then case-sensitive.
935
+ # I don't know anybody who does this, but we'll handle the theoretical case of a
936
+ # camelCase column name. I imagine other dbs handle this different, since there's a
937
+ # unit test that's currently failing test_oci.
938
+ def oracle_downcase(column_name)
939
+ column_name =~ /[a-z]/ ? column_name : column_name.downcase
940
+ end
941
+
942
+ end
943
+ end
944
+ end
945
+
946
+
947
+ class OCI8 #:nodoc:
948
+
949
+ # This OCI8 patch may not longer be required with the upcoming
950
+ # release of version 0.2.
951
+ class Cursor #:nodoc:
952
+ alias :enhanced_define_a_column_pre_ar :define_a_column
953
+ def define_a_column(i)
954
+ case do_ocicall(@ctx) { @parms[i - 1].attrGet(OCI_ATTR_DATA_TYPE) }
955
+ when 8; @stmt.defineByPos(i, String, 65535) # Read LONG values
956
+ when 187; @stmt.defineByPos(i, OraDate) # Read TIMESTAMP values
957
+ when 108
958
+ if @parms[i - 1].attrGet(OCI_ATTR_TYPE_NAME) == 'XMLTYPE'
959
+ @stmt.defineByPos(i, String, 65535)
960
+ else
961
+ raise 'unsupported datatype'
962
+ end
963
+ else enhanced_define_a_column_pre_ar i
964
+ end
965
+ end
966
+ end
967
+
968
+ # missing constant from oci8 < 0.1.14
969
+ OCI_PTYPE_UNK = 0 unless defined?(OCI_PTYPE_UNK)
970
+
971
+ # Uses the describeAny OCI call to find the target owner and table_name
972
+ # indicated by +name+, parsing through synonynms as necessary. Returns
973
+ # an array of [owner, table_name].
974
+ def describe(name)
975
+ @desc ||= @@env.alloc(OCIDescribe)
976
+ @desc.attrSet(OCI_ATTR_DESC_PUBLIC, -1) if VERSION >= '0.1.14'
977
+ do_ocicall(@ctx) { @desc.describeAny(@svc, name.to_s, OCI_PTYPE_UNK) } rescue raise %Q{"DESC #{name}" failed; does it exist?}
978
+ info = @desc.attrGet(OCI_ATTR_PARAM)
979
+
980
+ case info.attrGet(OCI_ATTR_PTYPE)
981
+ when OCI_PTYPE_TABLE, OCI_PTYPE_VIEW
982
+ owner = info.attrGet(OCI_ATTR_OBJ_SCHEMA)
983
+ table_name = info.attrGet(OCI_ATTR_OBJ_NAME)
984
+ [owner, table_name]
985
+ when OCI_PTYPE_SYN
986
+ schema = info.attrGet(OCI_ATTR_SCHEMA_NAME)
987
+ name = info.attrGet(OCI_ATTR_NAME)
988
+ describe(schema + '.' + name)
989
+ else raise %Q{"DESC #{name}" failed; not a table or view.}
990
+ end
991
+ end
992
+
993
+ end
994
+
995
+
996
+ # The OracleConnectionFactory factors out the code necessary to connect and
997
+ # configure an Oracle/OCI connection.
998
+ class OracleEnhancedConnectionFactory #:nodoc:
999
+ def new_connection(username, password, database, async, prefetch_rows, cursor_sharing, privilege)
1000
+ conn = OCI8.new username, password, database, privilege
1001
+ conn.exec %q{alter session set nls_date_format = 'YYYY-MM-DD HH24:MI:SS'}
1002
+ conn.exec %q{alter session set nls_timestamp_format = 'YYYY-MM-DD HH24:MI:SS'} rescue nil
1003
+ conn.autocommit = true
1004
+ conn.non_blocking = true if async
1005
+ conn.prefetch_rows = prefetch_rows
1006
+ conn.exec "alter session set cursor_sharing = #{cursor_sharing}" rescue nil
1007
+ conn
1008
+ end
1009
+ end
1010
+
1011
+
1012
+ # The OCI8AutoRecover class enhances the OCI8 driver with auto-recover and
1013
+ # reset functionality. If a call to #exec fails, and autocommit is turned on
1014
+ # (ie., we're not in the middle of a longer transaction), it will
1015
+ # automatically reconnect and try again. If autocommit is turned off,
1016
+ # this would be dangerous (as the earlier part of the implied transaction
1017
+ # may have failed silently if the connection died) -- so instead the
1018
+ # connection is marked as dead, to be reconnected on it's next use.
1019
+ class OCI8EnhancedAutoRecover < DelegateClass(OCI8) #:nodoc:
1020
+ attr_accessor :active
1021
+ alias :active? :active
1022
+
1023
+ cattr_accessor :auto_retry
1024
+ class << self
1025
+ alias :auto_retry? :auto_retry
1026
+ end
1027
+ @@auto_retry = false
1028
+
1029
+ def initialize(config, factory = OracleEnhancedConnectionFactory.new)
1030
+ @active = true
1031
+ @username, @password, @database, = config[:username].to_s, config[:password].to_s, config[:database].to_s
1032
+ @async = config[:allow_concurrency]
1033
+ @prefetch_rows = config[:prefetch_rows] || 100
1034
+ @cursor_sharing = config[:cursor_sharing] || 'similar'
1035
+ @factory = factory
1036
+ @privilege = config[:privilege]
1037
+ @connection = @factory.new_connection @username, @password, @database, @async, @prefetch_rows, @cursor_sharing, @privilege
1038
+ super @connection
1039
+ end
1040
+
1041
+ # Checks connection, returns true if active. Note that ping actively
1042
+ # checks the connection, while #active? simply returns the last
1043
+ # known state.
1044
+ def ping
1045
+ @connection.exec("select 1 from dual") { |r| nil }
1046
+ @active = true
1047
+ rescue
1048
+ @active = false
1049
+ raise
1050
+ end
1051
+
1052
+ # Resets connection, by logging off and creating a new connection.
1053
+ def reset!
1054
+ logoff rescue nil
1055
+ begin
1056
+ @connection = @factory.new_connection @username, @password, @database, @async, @prefetch_rows, @cursor_sharing, @privilege
1057
+ __setobj__ @connection
1058
+ @active = true
1059
+ rescue
1060
+ @active = false
1061
+ raise
1062
+ end
1063
+ end
1064
+
1065
+ # ORA-00028: your session has been killed
1066
+ # ORA-01012: not logged on
1067
+ # ORA-03113: end-of-file on communication channel
1068
+ # ORA-03114: not connected to ORACLE
1069
+ LOST_CONNECTION_ERROR_CODES = [ 28, 1012, 3113, 3114 ]
1070
+
1071
+ # Adds auto-recovery functionality.
1072
+ #
1073
+ # See: http://www.jiubao.org/ruby-oci8/api.en.html#label-11
1074
+ def exec(sql, *bindvars, &block)
1075
+ should_retry = self.class.auto_retry? && autocommit?
1076
+
1077
+ begin
1078
+ @connection.exec(sql, *bindvars, &block)
1079
+ rescue OCIException => e
1080
+ raise unless LOST_CONNECTION_ERROR_CODES.include?(e.code)
1081
+ @active = false
1082
+ raise unless should_retry
1083
+ should_retry = false
1084
+ reset! rescue nil
1085
+ retry
1086
+ end
1087
+ end
1088
+
1089
+ end
1090
+
1091
+ rescue LoadError
1092
+ # OCI8 driver is unavailable.
1093
+ if defined?(RAILS_DEFAULT_LOGGER)
1094
+ RAILS_DEFAULT_LOGGER.error "ERROR: ActiveRecord oracle_enhanced adapter could not load ruby-oci8 library. "+
1095
+ "Please install ruby-oci8 library or gem."
1096
+ end
1097
+ module ActiveRecord # :nodoc:
1098
+ class Base
1099
+ @@oracle_error_message = "Oracle/OCI libraries could not be loaded: #{$!.to_s}"
1100
+ def self.oracle_enhanced_connection(config) # :nodoc:
1101
+ # Set up a reasonable error message
1102
+ raise LoadError, @@oracle_error_message
1103
+ end
1104
+ end
1105
+ end
1106
+ end
1107
+
1108
+ # RSI: Added LOB writing callback for sessions stored in database
1109
+ # Otherwise it is not working as Session class is defined before OracleAdapter is loaded in Rails 2.0
1110
+ if defined?(CGI::Session::ActiveRecordStore::Session)
1111
+ if !CGI::Session::ActiveRecordStore::Session.respond_to?(:after_save_callback_chain) ||
1112
+ CGI::Session::ActiveRecordStore::Session.after_save_callback_chain.detect{|cb| cb.method == :enhanced_write_lobs}.nil?
1113
+ class CGI::Session::ActiveRecordStore::Session
1114
+ after_save :enhanced_write_lobs
1115
+ end
1116
+ end
1117
+ end
1118
+
1119
+ # RSI: load custom create, update, delete methods functionality
1120
+ # rescue LoadError if ruby-plsql gem cannot be loaded
1121
+ begin
1122
+ require 'active_record/connection_adapters/oracle_enhanced_procedures'
1123
+ rescue LoadError
1124
+ if defined?(RAILS_DEFAULT_LOGGER)
1125
+ RAILS_DEFAULT_LOGGER.info "INFO: ActiveRecord oracle_enhanced adapter could not load ruby-plsql gem. "+
1126
+ "Custom create, update and delete methods will not be available."
1127
+ end
1128
+ end
1129
+
1130
+ # RSI: load additional methods for composite_primary_keys support
1131
+ require 'active_record/connection_adapters/oracle_enhanced_cpk'
1132
+
1133
+ # RSI: load patch for dirty tracking methods
1134
+ require 'active_record/connection_adapters/oracle_enhanced_dirty'
1135
+
1136
+ # handles quoting of oracle reserved words
1137
+ require 'active_record/connection_adapters/oracle_enhanced_reserved_words'