rwc9u-oracle-enhanced 1.1.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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'