rwc9u-activerecord-oracle_enhanced-adapter 1.1.9.5 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (22) hide show
  1. data/History.txt +11 -0
  2. data/Manifest.txt +6 -0
  3. data/README.txt +6 -2
  4. data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +757 -909
  5. data/lib/active_record/connection_adapters/oracle_enhanced_connection.rb +71 -0
  6. data/lib/active_record/connection_adapters/oracle_enhanced_core_ext.rb +64 -0
  7. data/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb +2 -2
  8. data/lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb +352 -0
  9. data/lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb +346 -0
  10. data/lib/active_record/connection_adapters/oracle_enhanced_procedures.rb +2 -1
  11. data/lib/active_record/connection_adapters/oracle_enhanced_version.rb +2 -2
  12. data/oracle-enhanced.gemspec +32 -8
  13. data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +111 -93
  14. data/spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb +170 -0
  15. data/spec/active_record/connection_adapters/oracle_enhanced_core_ext_spec.rb +40 -0
  16. data/spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb +11 -6
  17. data/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +148 -53
  18. data/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb +13 -5
  19. data/spec/active_record/connection_adapters/oracle_enhanced_emulate_oracle_adapter_spec.rb +12 -6
  20. data/spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb +10 -6
  21. data/spec/spec_helper.rb +51 -6
  22. metadata +13 -6
data/History.txt CHANGED
@@ -1,3 +1,14 @@
1
+ == 1.2.0 2009-03-22
2
+
3
+ * Enhancements
4
+ * support for JRuby and JDBC
5
+ * support for Ruby 1.9.1 and ruby-oci8 2.0
6
+ * support for Rails 2.3
7
+ * quoting of Oracle reserved words in table names and column names
8
+ * emulation of OracleAdapter (for ActiveRecord unit tests)
9
+ * Bug fixes:
10
+ * several bug fixes that were identified during running of ActiveRecord unit tests
11
+
1
12
  == 1.1.9 2009-01-02
2
13
 
3
14
  * Enhancements
data/Manifest.txt CHANGED
@@ -5,13 +5,19 @@ README.txt
5
5
  lib/active_record/connection_adapters/emulation/oracle_adapter.rb
6
6
  lib/active_record/connection_adapters/oracle_enhanced.rake
7
7
  lib/active_record/connection_adapters/oracle_enhanced_adapter.rb
8
+ lib/active_record/connection_adapters/oracle_enhanced_connection.rb
9
+ lib/active_record/connection_adapters/oracle_enhanced_core_ext.rb
8
10
  lib/active_record/connection_adapters/oracle_enhanced_cpk.rb
9
11
  lib/active_record/connection_adapters/oracle_enhanced_dirty.rb
12
+ lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb
13
+ lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb
10
14
  lib/active_record/connection_adapters/oracle_enhanced_procedures.rb
11
15
  lib/active_record/connection_adapters/oracle_enhanced_reserved_words.rb
12
16
  lib/active_record/connection_adapters/oracle_enhanced_tasks.rb
13
17
  lib/active_record/connection_adapters/oracle_enhanced_version.rb
14
18
  spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb
19
+ spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb
20
+ spec/active_record/connection_adapters/oracle_enhanced_core_ext_spec.rb
15
21
  spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb
16
22
  spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb
17
23
  spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb
data/README.txt CHANGED
@@ -17,8 +17,12 @@ Bugs and enhancement requests can be reported at http://rsim.lighthouseapp.com/p
17
17
 
18
18
  == REQUIREMENTS:
19
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
20
+ * Works (has been tested) with ActiveRecord version 2.0, 2.1, 2.2 and 2.3 (these are the same as Rails versions)
21
+ * Can be used on the following Ruby platforms:
22
+ * MRI - requires ruby-oci8 1.x or 2.x library to connect to Oracle
23
+ * Ruby/YARV 1.9.1 - requires ruby-oci8 2.x library to connect to Oracle
24
+ unicode_utils gem is recommended for Unicode aware string upcase and downcase
25
+ * JRuby - uses JDBC driver ojdbc14.jar to connect to Oracle (should be in JRUBY_HOME/lib or in PATH)
22
26
  * 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
27
 
24
28
  == INSTALL:
@@ -29,1078 +29,917 @@
29
29
  # portions Copyright 2005 Graham Jenkins
30
30
 
31
31
  require 'active_record/connection_adapters/abstract_adapter'
32
- require 'delegate'
33
32
 
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
33
+ require 'active_record/connection_adapters/oracle_enhanced_connection'
34
+
35
+ module ActiveRecord
36
+ class Base
37
+ def self.oracle_enhanced_connection(config)
38
+ if config[:emulate_oracle_adapter] == true
39
+ # allows the enhanced adapter to look like the OracleAdapter. Useful to pick up
40
+ # conditionals in the rails activerecord test suite
41
+ require 'active_record/connection_adapters/emulation/oracle_adapter'
42
+ ConnectionAdapters::OracleAdapter.new(
43
+ ConnectionAdapters::OracleEnhancedConnection.create(config), logger)
44
+ else
45
+ ConnectionAdapters::OracleEnhancedAdapter.new(
46
+ ConnectionAdapters::OracleEnhancedConnection.create(config), logger)
60
47
  end
48
+ end
61
49
 
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
50
+ # RSI: specify table columns which should be ifnored
51
+ def self.ignore_table_columns(*args)
52
+ connection.ignore_table_columns(table_name,*args)
53
+ end
66
54
 
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
55
+ # RSI: specify which table columns should be treated as date (without time)
56
+ def self.set_date_columns(*args)
57
+ connection.set_type_for_columns(table_name,:date,*args)
58
+ end
71
59
 
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
60
+ # RSI: specify which table columns should be treated as datetime
61
+ def self.set_datetime_columns(*args)
62
+ connection.set_type_for_columns(table_name,:datetime,*args)
63
+ end
76
64
 
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
65
+ # RSI: specify which table columns should be treated as booleans
66
+ def self.set_boolean_columns(*args)
67
+ connection.set_type_for_columns(table_name,:boolean,*args)
68
+ end
81
69
 
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
70
+ # After setting large objects to empty, select the OCI8::LOB
71
+ # and write back the data.
72
+ after_save :enhanced_write_lobs
73
+ def enhanced_write_lobs #:nodoc:
74
+ if connection.is_a?(ConnectionAdapters::OracleEnhancedAdapter) &&
75
+ !(self.class.custom_create_method || self.class.custom_update_method)
76
+ connection.write_lobs(self.class.table_name, self.class, attributes)
90
77
  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
78
+ end
79
+ private :enhanced_write_lobs
80
+
81
+ class << self
82
+ # RSI: patch ORDER BY to work with LOBs
83
+ def add_order_with_lobs!(sql, order, scope = :auto)
84
+ if connection.is_a?(ConnectionAdapters::OracleEnhancedAdapter)
85
+ order = connection.lob_order_by_expression(self, order) if order
86
+
87
+ orig_scope = scope
88
+ scope = scope(:find) if :auto == scope
89
+ if scope
90
+ new_scope_order = connection.lob_order_by_expression(self, scope[:order])
91
+ if new_scope_order != scope[:order]
92
+ scope = scope.merge(:order => new_scope_order)
93
+ else
94
+ scope = orig_scope
108
95
  end
109
96
  end
110
- add_order_without_lobs!(sql, order, scope = :auto)
111
97
  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!
98
+ add_order_without_lobs!(sql, order, scope = :auto)
115
99
  end
100
+ private :add_order_with_lobs!
101
+ alias_method :add_order_without_lobs!, :add_order!
102
+ alias_method :add_order!, :add_order_with_lobs!
103
+ end
116
104
 
117
- # RSI: get table comment from schema definition
118
- def self.table_comment
119
- self.connection.table_comment(self.table_name)
120
- end
105
+ # RSI: get table comment from schema definition
106
+ def self.table_comment
107
+ connection.table_comment(self.table_name)
121
108
  end
109
+ end
122
110
 
123
111
 
124
- module ConnectionAdapters #:nodoc:
125
- class OracleEnhancedColumn < Column #:nodoc:
112
+ module ConnectionAdapters #:nodoc:
113
+ class OracleEnhancedColumn < Column #:nodoc:
126
114
 
127
- attr_reader :table_name, :forced_column_type
115
+ attr_reader :table_name, :forced_column_type
128
116
 
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
117
+ def initialize(name, default, sql_type = nil, null = true, table_name = nil, forced_column_type = nil)
118
+ @table_name = table_name
119
+ @forced_column_type = forced_column_type
120
+ super(name, default, sql_type, null)
121
+ end
134
122
 
135
- def type_cast(value)
136
- return guess_date_or_time(value) if type == :datetime && OracleEnhancedAdapter.emulate_dates
137
- super
138
- end
123
+ def type_cast(value)
124
+ return guess_date_or_time(value) if type == :datetime && OracleEnhancedAdapter.emulate_dates
125
+ super
126
+ end
139
127
 
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
128
+ # convert something to a boolean
129
+ # RSI: added y as boolean value
130
+ def self.value_to_boolean(value)
131
+ if value == true || value == false
132
+ value
133
+ elsif value.is_a?(String) && value.blank?
134
+ nil
135
+ else
136
+ %w(true t 1 y +).include?(value.to_s.downcase)
148
137
  end
138
+ end
149
139
 
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
140
+ # RSI: convert Time value to Date for :date columns
141
+ def self.string_to_date(string)
142
+ return string.to_date if string.is_a?(Time)
143
+ super
144
+ end
155
145
 
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
146
+ # RSI: convert Date value to Time for :datetime columns
147
+ def self.string_to_time(string)
148
+ return string.to_time if string.is_a?(Date) && !OracleEnhancedAdapter.emulate_dates
149
+ super
150
+ end
161
151
 
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
152
+ # RSI: get column comment from schema definition
153
+ # will work only if using default ActiveRecord connection
154
+ def comment
155
+ ActiveRecord::Base.connection.column_comment(@table_name, name)
156
+ end
167
157
 
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
158
+ private
159
+ def simplified_type(field_type)
160
+ return :boolean if OracleEnhancedAdapter.emulate_booleans && field_type == 'NUMBER(1)'
161
+ return :boolean if OracleEnhancedAdapter.emulate_booleans_from_strings &&
162
+ (forced_column_type == :boolean ||
163
+ OracleEnhancedAdapter.is_boolean_column?(name, field_type, table_name))
164
+
165
+ case field_type
166
+ when /date/i
167
+ forced_column_type ||
168
+ (:date if OracleEnhancedAdapter.emulate_dates_by_column_name && OracleEnhancedAdapter.is_date_column?(name, table_name)) ||
169
+ :datetime
170
+ when /timestamp/i then :timestamp
171
+ when /time/i then :datetime
172
+ when /decimal|numeric|number/i
173
+ return :integer if extract_scale(field_type) == 0
174
+ # RSI: if column name is ID or ends with _ID
175
+ return :integer if OracleEnhancedAdapter.emulate_integers_by_column_name && OracleEnhancedAdapter.is_integer_column?(name, table_name)
176
+ :decimal
177
+ else super
189
178
  end
179
+ end
190
180
 
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
181
+ def guess_date_or_time(value)
182
+ value.respond_to?(:hour) && (value.hour == 0 and value.min == 0 and value.sec == 0) ?
183
+ Date.new(value.year, value.month, value.day) : value
184
+ end
195
185
 
196
- class << self
197
- protected
186
+ class <<self
187
+ protected
198
188
 
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
189
+ def fallback_string_to_date(string)
190
+ if OracleEnhancedAdapter.string_to_date_format || OracleEnhancedAdapter.string_to_time_format
191
+ return (string_to_date_or_time_using_format(string).to_date rescue super)
204
192
  end
193
+ super
194
+ end
205
195
 
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
196
+ def fallback_string_to_time(string)
197
+ if OracleEnhancedAdapter.string_to_time_format || OracleEnhancedAdapter.string_to_date_format
198
+ return (string_to_date_or_time_using_format(string).to_time rescue super)
211
199
  end
200
+ super
201
+ end
212
202
 
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)
203
+ def string_to_date_or_time_using_format(string)
204
+ if OracleEnhancedAdapter.string_to_time_format && dt=Date._strptime(string, OracleEnhancedAdapter.string_to_time_format)
205
+ return Time.mktime(*dt.values_at(:year, :mon, :mday, :hour, :min, :sec, :zone, :wday))
218
206
  end
219
-
207
+ DateTime.strptime(string, OracleEnhancedAdapter.string_to_date_format)
220
208
  end
209
+
221
210
  end
211
+ end
222
212
 
223
213
 
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
214
+ # This is an Oracle/OCI adapter for the ActiveRecord persistence
215
+ # framework. It relies upon the OCI8 driver, which works with Oracle 8i
216
+ # and above. Most recent development has been on Debian Linux against
217
+ # a 10g database, ActiveRecord 1.12.1 and OCI8 0.1.13.
218
+ # See: http://rubyforge.org/projects/ruby-oci8/
219
+ #
220
+ # Usage notes:
221
+ # * Key generation assumes a "${table_name}_seq" sequence is available
222
+ # for all tables; the sequence name can be changed using
223
+ # ActiveRecord::Base.set_sequence_name. When using Migrations, these
224
+ # sequences are created automatically.
225
+ # * Oracle uses DATE or TIMESTAMP datatypes for both dates and times.
226
+ # Consequently some hacks are employed to map data back to Date or Time
227
+ # in Ruby. If the column_name ends in _time it's created as a Ruby Time.
228
+ # Else if the hours/minutes/seconds are 0, I make it a Ruby Date. Else
229
+ # it's a Ruby Time. This is a bit nasty - but if you use Duck Typing
230
+ # you'll probably not care very much. In 9i and up it's tempting to
231
+ # map DATE to Date and TIMESTAMP to Time, but too many databases use
232
+ # DATE for both. Timezones and sub-second precision on timestamps are
233
+ # not supported.
234
+ # * Default values that are functions (such as "SYSDATE") are not
235
+ # supported. This is a restriction of the way ActiveRecord supports
236
+ # default values.
237
+ # * Support for Oracle8 is limited by Rails' use of ANSI join syntax, which
238
+ # is supported in Oracle9i and later. You will need to use #finder_sql for
239
+ # has_and_belongs_to_many associations to run against Oracle8.
240
+ #
241
+ # Required parameters:
242
+ #
243
+ # * <tt>:username</tt>
244
+ # * <tt>:password</tt>
245
+ # * <tt>:database</tt>
246
+ class OracleEnhancedAdapter < AbstractAdapter
247
+
248
+ @@emulate_booleans = true
249
+ cattr_accessor :emulate_booleans
250
+
251
+ @@emulate_dates = false
252
+ cattr_accessor :emulate_dates
253
+
254
+ # RSI: set to true if columns with DATE in their name should be emulated as date
255
+ @@emulate_dates_by_column_name = false
256
+ cattr_accessor :emulate_dates_by_column_name
257
+ def self.is_date_column?(name, table_name = nil)
258
+ name =~ /(^|_)date(_|$)/i
259
+ end
260
+ # RSI: instance method uses at first check if column type defined at class level
261
+ def is_date_column?(name, table_name = nil)
262
+ case get_type_for_column(table_name, name)
263
+ when nil
264
+ self.class.is_date_column?(name, table_name)
265
+ when :date
266
+ true
267
+ else
268
+ false
280
269
  end
270
+ end
281
271
 
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
272
+ # RSI: set to true if NUMBER columns with ID at the end of their name should be emulated as integers
273
+ @@emulate_integers_by_column_name = false
274
+ cattr_accessor :emulate_integers_by_column_name
275
+ def self.is_integer_column?(name, table_name = nil)
276
+ name =~ /(^|_)id$/i
277
+ end
288
278
 
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
279
+ # RSI: set to true if CHAR(1), VARCHAR2(1) columns or VARCHAR2 columns with FLAG or YN at the end of their name
280
+ # should be emulated as booleans
281
+ @@emulate_booleans_from_strings = false
282
+ cattr_accessor :emulate_booleans_from_strings
283
+ def self.is_boolean_column?(name, field_type, table_name = nil)
284
+ return true if ["CHAR(1)","VARCHAR2(1)"].include?(field_type)
285
+ field_type =~ /^VARCHAR2/ && (name =~ /_flag$/i || name =~ /_yn$/i)
286
+ end
287
+ def self.boolean_to_string(bool)
288
+ bool ? "Y" : "N"
289
+ end
300
290
 
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
291
+ # RSI: use to set NLS specific date formats which will be used when assigning string to :date and :datetime columns
292
+ @@string_to_date_format = @@string_to_time_format = nil
293
+ cattr_accessor :string_to_date_format, :string_to_time_format
304
294
 
305
- def adapter_name #:nodoc:
306
- 'OracleEnhanced'
307
- end
295
+ def adapter_name #:nodoc:
296
+ 'OracleEnhanced'
297
+ end
308
298
 
309
- def supports_migrations? #:nodoc:
310
- true
311
- end
299
+ def supports_migrations? #:nodoc:
300
+ true
301
+ end
312
302
 
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
303
+ def native_database_types #:nodoc:
304
+ {
305
+ :primary_key => "NUMBER(38) NOT NULL PRIMARY KEY",
306
+ :string => { :name => "VARCHAR2", :limit => 255 },
307
+ :text => { :name => "CLOB" },
308
+ :integer => { :name => "NUMBER", :limit => 38 },
309
+ :float => { :name => "NUMBER" },
310
+ :decimal => { :name => "DECIMAL" },
311
+ :datetime => { :name => "DATE" },
312
+ # RSI: changed to native TIMESTAMP type
313
+ # :timestamp => { :name => "DATE" },
314
+ :timestamp => { :name => "TIMESTAMP" },
315
+ :time => { :name => "DATE" },
316
+ :date => { :name => "DATE" },
317
+ :binary => { :name => "BLOB" },
318
+ # RSI: if emulate_booleans_from_strings then store booleans in VARCHAR2
319
+ :boolean => emulate_booleans_from_strings ?
320
+ { :name => "VARCHAR2", :limit => 1 } : { :name => "NUMBER", :limit => 1 }
321
+ }
322
+ end
333
323
 
334
- def table_alias_length
335
- 30
336
- end
324
+ def table_alias_length
325
+ 30
326
+ end
337
327
 
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
328
+ # Returns an array of arrays containing the field values.
329
+ # Order is the same as that returned by #columns.
330
+ def select_rows(sql, name = nil)
331
+ # last parameter indicates to return also column list
332
+ result, columns = select(sql, name, true)
333
+ result.map{ |v| columns.map{|c| v[c]} }
334
+ end
344
335
 
345
- # QUOTING ==================================================
346
- #
347
- # see: abstract/quoting.rb
336
+ # QUOTING ==================================================
337
+ #
338
+ # see: abstract/quoting.rb
348
339
 
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
340
+ # camelCase column names need to be quoted; not that anyone using Oracle
341
+ # would really do this, but handling this case means we pass the test...
342
+ def quote_column_name(name) #:nodoc:
343
+ name.to_s =~ /[A-Z]/ ? "\"#{name}\"" : quote_oracle_reserved_words(name)
344
+ end
354
345
 
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
346
+ # unescaped table name should start with letter and
347
+ # contain letters, digits, _, $ or #
348
+ # can be prefixed with schema name
349
+ def self.valid_table_name?(name)
350
+ name.to_s =~ /^([A-Z_0-9]+\.)?[A-Z][A-Z_0-9\$#]*$/i ? true : false
351
+ end
364
352
 
365
- def quote_string(s) #:nodoc:
366
- s.gsub(/'/, "''")
353
+ # abstract_adapter calls quote_column_name from quote_table_name, so prevent that
354
+ def quote_table_name(name)
355
+ if self.class.valid_table_name?(name)
356
+ name
357
+ else
358
+ "\"#{name}\""
367
359
  end
360
+ end
368
361
 
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)
362
+ def quote_string(s) #:nodoc:
363
+ s.gsub(/'/, "''")
364
+ end
365
+
366
+ def quote(value, column = nil) #:nodoc:
367
+ if value && column
368
+ case column.type
369
+ when :text, :binary
370
+ %Q{empty_#{ column.sql_type.downcase rescue 'blob' }()}
371
+ # RSI: TIMESTAMP support
372
+ when :timestamp
373
+ quote_timestamp_with_to_timestamp(value)
374
+ # RSI: NLS_DATE_FORMAT independent DATE support
375
+ when :date, :time, :datetime
384
376
  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
377
  else
388
378
  super
389
379
  end
380
+ elsif value.acts_like?(:date)
381
+ quote_date_with_to_date(value)
382
+ elsif value.acts_like?(:time)
383
+ value.to_i == value.to_f ? quote_date_with_to_date(value) : quote_timestamp_with_to_timestamp(value)
384
+ else
385
+ super
390
386
  end
387
+ end
391
388
 
392
- def quoted_true
393
- return "'#{self.class.boolean_to_string(true)}'" if emulate_booleans_from_strings
394
- "1"
395
- end
389
+ def quoted_true
390
+ return "'#{self.class.boolean_to_string(true)}'" if emulate_booleans_from_strings
391
+ "1"
392
+ end
396
393
 
397
- def quoted_false
398
- return "'#{self.class.boolean_to_string(false)}'" if emulate_booleans_from_strings
399
- "0"
400
- end
394
+ def quoted_false
395
+ return "'#{self.class.boolean_to_string(false)}'" if emulate_booleans_from_strings
396
+ "0"
397
+ end
401
398
 
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
399
+ # RSI: should support that composite_primary_keys gem will pass date as string
400
+ def quote_date_with_to_date(value)
401
+ value = value.to_s(:db) if value.acts_like?(:date) || value.acts_like?(:time)
402
+ "TO_DATE('#{value}','YYYY-MM-DD HH24:MI:SS')"
403
+ end
407
404
 
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
405
+ def quote_timestamp_with_to_timestamp(value)
406
+ # add up to 9 digits of fractional seconds to inserted time
407
+ value = "#{value.to_s(:db)}.#{("%.6f"%value.to_f).split('.')[1]}" if value.acts_like?(:time)
408
+ "TO_TIMESTAMP('#{value}','YYYY-MM-DD HH24:MI:SS.FF6')"
409
+ end
413
410
 
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
411
+ # CONNECTION MANAGEMENT ====================================
412
+ #
427
413
 
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
414
+ # If SQL statement fails due to lost connection then reconnect
415
+ # and retry SQL statement if autocommit mode is enabled.
416
+ # By default this functionality is disabled.
417
+ @auto_retry = false
418
+ attr_reader :auto_retry
419
+ def auto_retry=(value)
420
+ @auto_retry = value
421
+ @connection.auto_retry = value if @connection
422
+ end
434
423
 
435
- # Disconnects from the database.
436
- def disconnect!
437
- @connection.logoff rescue nil
438
- @connection.active = false
439
- end
424
+ def raw_connection
425
+ @connection.raw_connection
426
+ end
440
427
 
428
+ # Returns true if the connection is active.
429
+ def active?
430
+ # Pings the connection to check if it's still good. Note that an
431
+ # #active? method is also available, but that simply returns the
432
+ # last known state, which isn't good enough if the connection has
433
+ # gone stale since the last use.
434
+ @connection.ping
435
+ rescue OracleEnhancedConnectionException
436
+ false
437
+ end
441
438
 
442
- # DATABASE STATEMENTS ======================================
443
- #
444
- # see: abstract/database_statements.rb
439
+ # Reconnects to the database.
440
+ def reconnect!
441
+ @connection.reset!
442
+ rescue OracleEnhancedConnectionException => e
443
+ @logger.warn "#{adapter_name} automatic reconnection failed: #{e.message}"
444
+ end
445
445
 
446
- def execute(sql, name = nil) #:nodoc:
447
- log(sql, name) { @connection.exec sql }
448
- end
446
+ # Disconnects from the database.
447
+ def disconnect!
448
+ @connection.logoff rescue nil
449
+ end
449
450
 
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
451
 
459
- def begin_db_transaction #:nodoc:
460
- @connection.autocommit = false
461
- end
452
+ # DATABASE STATEMENTS ======================================
453
+ #
454
+ # see: abstract/database_statements.rb
462
455
 
463
- def commit_db_transaction #:nodoc:
464
- @connection.commit
465
- ensure
466
- @connection.autocommit = true
467
- end
456
+ def execute(sql, name = nil) #:nodoc:
457
+ log(sql, name) { @connection.exec sql }
458
+ end
468
459
 
469
- def rollback_db_transaction #:nodoc:
470
- @connection.rollback
471
- ensure
472
- @connection.autocommit = true
473
- end
460
+ # Returns the next sequence value from a sequence generator. Not generally
461
+ # called directly; used by ActiveRecord to get the next primary key value
462
+ # when inserting a new database record (see #prefetch_primary_key?).
463
+ def next_sequence_value(sequence_name)
464
+ select_one("select #{sequence_name}.nextval id from dual")['id']
465
+ end
474
466
 
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
467
+ def begin_db_transaction #:nodoc:
468
+ @connection.autocommit = false
469
+ end
478
470
 
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
471
+ def commit_db_transaction #:nodoc:
472
+ @connection.commit
473
+ ensure
474
+ @connection.autocommit = true
475
+ end
486
476
 
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
477
+ def rollback_db_transaction #:nodoc:
478
+ @connection.rollback
479
+ ensure
480
+ @connection.autocommit = true
481
+ end
492
482
 
493
- def default_sequence_name(table, column) #:nodoc:
494
- quote_table_name("#{table}_seq")
483
+ def add_limit_offset!(sql, options) #:nodoc:
484
+ # RSI: added to_i for limit and offset to protect from SQL injection
485
+ offset = (options[:offset] || 0).to_i
486
+
487
+ if limit = options[:limit]
488
+ limit = limit.to_i
489
+ sql.replace "select * from (select raw_sql_.*, rownum raw_rnum_ from (#{sql}) raw_sql_ where rownum <= #{offset+limit}) where raw_rnum_ > #{offset}"
490
+ elsif offset > 0
491
+ sql.replace "select * from (select raw_sql_.*, rownum raw_rnum_ from (#{sql}) raw_sql_) where raw_rnum_ > #{offset}"
495
492
  end
493
+ end
496
494
 
495
+ # Returns true for Oracle adapter (since Oracle requires primary key
496
+ # values to be pre-fetched before insert). See also #next_sequence_value.
497
+ def prefetch_primary_key?(table_name = nil)
498
+ true
499
+ end
497
500
 
498
- # Inserts the given fixture into the table. Overridden to properly handle lobs.
499
- def insert_fixture(fixture, table_name)
500
- super
501
+ def default_sequence_name(table, column) #:nodoc:
502
+ quote_table_name("#{table}_seq")
503
+ end
501
504
 
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
505
+
506
+ # Inserts the given fixture into the table. Overridden to properly handle lobs.
507
+ def insert_fixture(fixture, table_name)
508
+ super
509
+
510
+ klass = fixture.class_name.constantize rescue nil
511
+ if klass.respond_to?(:ancestors) && klass.ancestors.include?(ActiveRecord::Base)
512
+ write_lobs(table_name, klass, fixture)
506
513
  end
514
+ end
507
515
 
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",
516
+ # Writes LOB values from attributes, as indicated by the LOB columns of klass.
517
+ def write_lobs(table_name, klass, attributes)
518
+ id = quote(attributes[klass.primary_key])
519
+ klass.columns.select { |col| col.sql_type =~ /LOB$/i }.each do |col|
520
+ value = attributes[col.name]
521
+ # RSI: changed sequence of next two lines - should check if value is nil before converting to yaml
522
+ next if value.nil? || (value == '')
523
+ value = value.to_yaml if col.text? && klass.serialized_attributes[col.name]
524
+ uncached do
525
+ lob = select_one("SELECT #{col.name} FROM #{table_name} WHERE #{klass.primary_key} = #{id} FOR UPDATE",
518
526
  'Writable Large Object')[col.name]
519
- lob.write value
520
- end
527
+ @connection.write_lob(lob, value, col.type == :binary)
521
528
  end
522
529
  end
530
+ end
523
531
 
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
532
+ # RSI: change LOB column for ORDER BY clause
533
+ # just first 100 characters are taken for ordering
534
+ def lob_order_by_expression(klass, order)
535
+ return order if order.nil?
536
+ changed = false
537
+ new_order = order.to_s.strip.split(/, */).map do |order_by_col|
538
+ column_name, asc_desc = order_by_col.split(/ +/)
539
+ if column = klass.columns.detect { |col| col.name == column_name && col.sql_type =~ /LOB$/i}
540
+ changed = true
541
+ "DBMS_LOB.SUBSTR(#{column_name},100,1) #{asc_desc}"
542
+ else
543
+ order_by_col
544
+ end
545
+ end.join(', ')
546
+ changed ? new_order : order
547
+ end
544
548
 
545
- def current_database #:nodoc:
546
- select_one("select sys_context('userenv','db_name') db from dual")["db"]
547
- end
549
+ # SCHEMA STATEMENTS ========================================
550
+ #
551
+ # see: abstract/schema_statements.rb
548
552
 
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
553
+ def current_database #:nodoc:
554
+ select_one("select sys_context('userenv','db_name') db from dual")["db"]
555
+ end
555
556
 
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
557
+ # RSI: changed select from user_tables to all_tables - much faster in large data dictionaries
558
+ def tables(name = nil) #:nodoc:
559
+ select_all("select lower(table_name) name from all_tables where owner = sys_context('userenv','session_user')").map {|t| t['name']}
560
+ end
575
561
 
576
- indexes.last.columns << row['column_name']
562
+ def indexes(table_name, name = nil) #:nodoc:
563
+ (owner, table_name) = @connection.describe(table_name)
564
+ result = select_all(<<-SQL, name)
565
+ SELECT lower(i.index_name) as index_name, i.uniqueness, lower(c.column_name) as column_name
566
+ FROM all_indexes i, all_ind_columns c
567
+ WHERE i.table_name = '#{table_name}'
568
+ AND i.owner = '#{owner}'
569
+ AND i.table_owner = '#{owner}'
570
+ AND c.index_name = i.index_name
571
+ AND c.index_owner = i.owner
572
+ AND NOT EXISTS (SELECT uc.index_name FROM all_constraints uc WHERE uc.index_name = i.index_name AND uc.owner = i.owner AND uc.constraint_type = 'P')
573
+ ORDER BY i.index_name, c.column_position
574
+ SQL
575
+
576
+ current_index = nil
577
+ indexes = []
578
+
579
+ result.each do |row|
580
+ if current_index != row['index_name']
581
+ indexes << IndexDefinition.new(table_name.downcase, row['index_name'], row['uniqueness'] == "UNIQUE", [])
582
+ current_index = row['index_name']
577
583
  end
578
584
 
579
- indexes
585
+ indexes.last.columns << row['column_name']
580
586
  end
581
587
 
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
588
+ indexes
589
+ end
589
590
 
590
- def ignored_table_columns(table_name)
591
- @ignore_table_columns ||= {}
592
- @ignore_table_columns[table_name]
593
- end
591
+ # RSI: set ignored columns for table
592
+ def ignore_table_columns(table_name, *args)
593
+ @ignore_table_columns ||= {}
594
+ @ignore_table_columns[table_name] ||= []
595
+ @ignore_table_columns[table_name] += args.map{|a| a.to_s.downcase}
596
+ @ignore_table_columns[table_name].uniq!
597
+ end
594
598
 
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
599
+ def ignored_table_columns(table_name)
600
+ @ignore_table_columns ||= {}
601
+ @ignore_table_columns[table_name]
602
+ end
603
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
604
+ # RSI: set explicit type for specified table columns
605
+ def set_type_for_columns(table_name, column_type, *args)
606
+ @table_column_type ||= {}
607
+ @table_column_type[table_name] ||= {}
608
+ args.each do |col|
609
+ @table_column_type[table_name][col.to_s.downcase] = column_type
607
610
  end
611
+ end
608
612
 
609
- def clear_types_for_columns
610
- @table_column_type = nil
611
- end
613
+ def get_type_for_column(table_name, column_name)
614
+ @table_column_type && @table_column_type[table_name] && @table_column_type[table_name][column_name.to_s.downcase]
615
+ end
612
616
 
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
617
+ def clear_types_for_columns
618
+ @table_column_type = nil
619
+ end
641
620
 
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
621
+ def columns(table_name, name = nil) #:nodoc:
622
+ # RSI: get ignored_columns by original table name
623
+ ignored_columns = ignored_table_columns(table_name)
624
+
625
+ (owner, desc_table_name) = @connection.describe(table_name)
626
+
627
+ table_cols = <<-SQL
628
+ select column_name as name, data_type as sql_type, data_default, nullable,
629
+ decode(data_type, 'NUMBER', data_precision,
630
+ 'FLOAT', data_precision,
631
+ 'VARCHAR2', data_length,
632
+ 'CHAR', data_length,
633
+ null) as limit,
634
+ decode(data_type, 'NUMBER', data_scale, null) as scale
635
+ from all_tab_columns
636
+ where owner = '#{owner}'
637
+ and table_name = '#{desc_table_name}'
638
+ order by column_id
639
+ SQL
640
+
641
+ # RSI: added deletion of ignored columns
642
+ select_all(table_cols, name).delete_if do |row|
643
+ ignored_columns && ignored_columns.include?(row['name'].downcase)
644
+ end.map do |row|
645
+ limit, scale = row['limit'], row['scale']
646
+ if limit || scale
647
+ row['sql_type'] << "(#{(limit || 38).to_i}" + ((scale = scale.to_i) > 0 ? ",#{scale})" : ")")
648
+ end
648
649
 
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'])))
650
+ # clean up odd default spacing from Oracle
651
+ if row['data_default']
652
+ row['data_default'].sub!(/^(.*?)\s*$/, '\1')
653
+ row['data_default'].sub!(/^'(.*)'$/, '\1')
654
+ row['data_default'] = nil if row['data_default'] =~ /^(null|empty_[bc]lob\(\))$/i
657
655
  end
656
+
657
+ OracleEnhancedColumn.new(oracle_downcase(row['name']),
658
+ row['data_default'],
659
+ row['sql_type'],
660
+ row['nullable'] == 'Y',
661
+ # RSI: pass table name for table specific column definitions
662
+ table_name,
663
+ # RSI: pass column type if specified in class definition
664
+ get_type_for_column(table_name, oracle_downcase(row['name'])))
658
665
  end
666
+ end
659
667
 
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
668
+ # RSI: default sequence start with value
669
+ @@default_sequence_start_value = 10000
670
+ cattr_accessor :default_sequence_start_value
671
+
672
+ def create_table(name, options = {}, &block) #:nodoc:
673
+ create_sequence = options[:id] != false
674
+ column_comments = {}
675
+ super(name, options) do |t|
676
+ # store that primary key was defined in create_table block
677
+ unless create_sequence
678
+ class <<t
679
+ attr_accessor :create_sequence
680
+ def primary_key(*args)
681
+ self.create_sequence = true
682
+ super(*args)
676
683
  end
677
684
  end
685
+ end
678
686
 
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)
687
+ # store column comments
688
+ class <<t
689
+ attr_accessor :column_comments
690
+ def column(name, type, options = {})
691
+ if options[:comment]
692
+ self.column_comments ||= {}
693
+ self.column_comments[name] = options[:comment]
688
694
  end
695
+ super(name, type, options)
689
696
  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
697
  end
704
698
 
699
+ result = block.call(t)
700
+ create_sequence = create_sequence || t.create_sequence
701
+ column_comments = t.column_comments if t.column_comments
705
702
  end
706
703
 
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
704
+ seq_name = options[:sequence_name] || quote_table_name("#{name}_seq")
705
+ seq_start_value = options[:sequence_start_value] || default_sequence_start_value
706
+ execute "CREATE SEQUENCE #{seq_name} START WITH #{seq_start_value}" if create_sequence
711
707
 
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
708
+ add_table_comment name, options[:comment]
709
+ column_comments.each do |column_name, comment|
710
+ add_comment name, column_name, comment
716
711
  end
717
712
 
718
- def remove_index(table_name, options = {}) #:nodoc:
719
- execute "DROP INDEX #{index_name(table_name, options)}"
720
- end
713
+ end
721
714
 
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
715
+ def rename_table(name, new_name) #:nodoc:
716
+ execute "RENAME #{quote_table_name(name)} TO #{quote_table_name(new_name)}"
717
+ execute "RENAME #{quote_table_name("#{name}_seq")} TO #{quote_table_name("#{new_name}_seq")}" rescue nil
718
+ end
725
719
 
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
720
+ def drop_table(name, options = {}) #:nodoc:
721
+ super(name)
722
+ seq_name = options[:sequence_name] || quote_table_name("#{name}_seq")
723
+ execute "DROP SEQUENCE #{seq_name}" rescue nil
724
+ end
731
725
 
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
726
+ def remove_index(table_name, options = {}) #:nodoc:
727
+ execute "DROP INDEX #{index_name(table_name, options)}"
728
+ end
735
729
 
736
- def remove_column(table_name, column_name) #:nodoc:
737
- execute "ALTER TABLE #{table_name} DROP COLUMN #{quote_column_name(column_name)}"
738
- end
730
+ def add_column(table_name, column_name, type, options = {})
731
+ add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
732
+ options[:type] = type
733
+ add_column_options!(add_column_sql, options)
734
+ execute(add_column_sql)
735
+ end
739
736
 
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
737
+ def change_column_default(table_name, column_name, default) #:nodoc:
738
+ execute "ALTER TABLE #{quote_table_name(table_name)} MODIFY #{quote_column_name(column_name)} DEFAULT #{quote(default)}"
739
+ end
745
740
 
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
741
+ def change_column_null(table_name, column_name, null, default = nil)
742
+ column = column_for(table_name, column_name)
750
743
 
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
744
+ unless null || default.nil?
745
+ execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
758
746
  end
759
747
 
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
748
+ change_column table_name, column_name, column.sql_type, :null => null
749
+ end
769
750
 
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
751
+ def change_column(table_name, column_name, type, options = {}) #:nodoc:
752
+ column = column_for(table_name, column_name)
753
+
754
+ # remove :null option if its value is the same as current column definition
755
+ # otherwise Oracle will raise error
756
+ if options.has_key?(:null) && options[:null] == column.null
757
+ options[:null] = nil
788
758
  end
789
759
 
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
760
+ change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} MODIFY #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
761
+ options[:type] = type
762
+ add_column_options!(change_column_sql, options)
763
+ execute(change_column_sql)
764
+ end
794
765
 
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
766
+ def rename_column(table_name, column_name, new_column_name) #:nodoc:
767
+ execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} to #{quote_column_name(new_column_name)}"
768
+ end
822
769
 
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
770
+ def remove_column(table_name, column_name) #:nodoc:
771
+ execute "ALTER TABLE #{quote_table_name(table_name)} DROP COLUMN #{quote_column_name(column_name)}"
772
+ end
827
773
 
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
774
+ # RSI: table and column comments
775
+ def add_comment(table_name, column_name, comment)
776
+ return if comment.blank?
777
+ execute "COMMENT ON COLUMN #{quote_table_name(table_name)}.#{column_name} IS '#{comment}'"
778
+ end
833
779
 
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
780
+ def add_table_comment(table_name, comment)
781
+ return if comment.blank?
782
+ execute "COMMENT ON TABLE #{quote_table_name(table_name)} IS '#{comment}'"
783
+ end
841
784
 
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
785
+ def table_comment(table_name)
786
+ (owner, table_name) = @connection.describe(table_name)
787
+ select_value <<-SQL
788
+ SELECT comments FROM all_tab_comments
789
+ WHERE owner = '#{owner}'
790
+ AND table_name = '#{table_name}'
791
+ SQL
792
+ end
864
793
 
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?
794
+ def column_comment(table_name, column_name)
795
+ (owner, table_name) = @connection.describe(table_name)
796
+ select_value <<-SQL
797
+ SELECT comments FROM all_col_comments
798
+ WHERE owner = '#{owner}'
799
+ AND table_name = '#{table_name}'
800
+ AND column_name = '#{column_name.upcase}'
801
+ SQL
802
+ end
870
803
 
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(', ')
804
+ # Find a table's primary key and sequence.
805
+ # *Note*: Only primary key is implemented - sequence will be nil.
806
+ def pk_and_sequence_for(table_name)
807
+ (owner, table_name) = @connection.describe(table_name)
808
+
809
+ # RSI: changed select from all_constraints to user_constraints - much faster in large data dictionaries
810
+ pks = select_values(<<-SQL, 'Primary Key')
811
+ select cc.column_name
812
+ from user_constraints c, user_cons_columns cc
813
+ where c.owner = '#{owner}'
814
+ and c.table_name = '#{table_name}'
815
+ and c.constraint_type = 'P'
816
+ and cc.owner = c.owner
817
+ and cc.constraint_name = c.constraint_name
818
+ SQL
819
+
820
+ # only support single column keys
821
+ pks.size == 1 ? [oracle_downcase(pks.first), nil] : nil
822
+ end
874
823
 
875
- sql << " ORDER BY #{order}"
824
+ def structure_dump #:nodoc:
825
+ s = select_all("select sequence_name from user_sequences").inject("") do |structure, seq|
826
+ structure << "create sequence #{seq.to_a.first.last};\n\n"
876
827
  end
877
828
 
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_'
829
+ # RSI: changed select from user_tables to all_tables - much faster in large data dictionaries
830
+ select_all("select table_name from all_tables where owner = sys_context('userenv','session_user')").inject(s) do |structure, table|
831
+ ddl = "create table #{table.to_a.first.last} (\n "
832
+ cols = select_all(%Q{
833
+ select column_name, data_type, data_length, char_used, char_length, data_precision, data_scale, data_default, nullable
834
+ from user_tab_columns
835
+ where table_name = '#{table.to_a.first.last}'
836
+ order by column_id
837
+ }).map do |row|
838
+ col = "#{row['column_name'].downcase} #{row['data_type'].downcase}"
839
+ if row['data_type'] =='NUMBER' and !row['data_precision'].nil?
840
+ col << "(#{row['data_precision'].to_i}"
841
+ col << ",#{row['data_scale'].to_i}" if !row['data_scale'].nil?
842
+ col << ')'
843
+ elsif row['data_type'].include?('CHAR')
844
+ length = row['char_used'] == 'C' ? row['char_length'].to_i : row['data_length'].to_i
845
+ col << "(#{length})"
922
846
  end
923
-
924
- rows << hash
847
+ col << " default #{row['data_default']}" if !row['data_default'].nil?
848
+ col << ' not null' if row['nullable'] == 'N'
849
+ col
925
850
  end
926
-
927
- rows
928
- ensure
929
- cursor.close if cursor
851
+ ddl << cols.join(",\n ")
852
+ ddl << ");\n\n"
853
+ structure << ddl
930
854
  end
855
+ end
931
856
 
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
857
+ def structure_drop #:nodoc:
858
+ s = select_all("select sequence_name from user_sequences").inject("") do |drop, seq|
859
+ drop << "drop sequence #{seq.to_a.first.last};\n\n"
940
860
  end
941
861
 
862
+ # RSI: changed select from user_tables to all_tables - much faster in large data dictionaries
863
+ select_all("select table_name from all_tables where owner = sys_context('userenv','session_user')").inject(s) do |drop, table|
864
+ drop << "drop table #{table.to_a.first.last} cascade constraints;\n\n"
865
+ end
942
866
  end
943
- end
944
- end
945
867
 
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)
868
+ def add_column_options!(sql, options) #:nodoc:
869
+ type = options[:type] || ((column = options[:column]) && column.type)
870
+ type = type && type.to_sym
871
+ # handle case of defaults for CLOB columns, which would otherwise get "quoted" incorrectly
872
+ if options_include_default?(options)
873
+ if type == :text
874
+ sql << " DEFAULT #{quote(options[:default])}"
960
875
  else
961
- raise 'unsupported datatype'
876
+ # from abstract adapter
877
+ sql << " DEFAULT #{quote(options[:default], options[:column])}"
962
878
  end
963
- else enhanced_define_a_column_pre_ar i
879
+ end
880
+ # must explicitly add NULL or NOT NULL to allow change_column to work on migrations
881
+ if options[:null] == false
882
+ sql << " NOT NULL"
883
+ elsif options[:null] == true
884
+ sql << " NULL" unless type == :primary_key
964
885
  end
965
886
  end
966
- end
967
887
 
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.}
888
+ # SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
889
+ #
890
+ # Oracle requires the ORDER BY columns to be in the SELECT list for DISTINCT
891
+ # queries. However, with those columns included in the SELECT DISTINCT list, you
892
+ # won't actually get a distinct list of the column you want (presuming the column
893
+ # has duplicates with multiple values for the ordered-by columns. So we use the
894
+ # FIRST_VALUE function to get a single (first) value for each column, effectively
895
+ # making every row the same.
896
+ #
897
+ # distinct("posts.id", "posts.created_at desc")
898
+ def distinct(columns, order_by)
899
+ return "DISTINCT #{columns}" if order_by.blank?
900
+
901
+ # construct a valid DISTINCT clause, ie. one that includes the ORDER BY columns, using
902
+ # FIRST_VALUE such that the inclusion of these columns doesn't invalidate the DISTINCT
903
+ order_columns = order_by.split(',').map { |s| s.strip }.reject(&:blank?)
904
+ order_columns = order_columns.zip((0...order_columns.size).to_a).map do |c, i|
905
+ "FIRST_VALUE(#{c.split.first}) OVER (PARTITION BY #{columns} ORDER BY #{c}) AS alias_#{i}__"
906
+ end
907
+ sql = "DISTINCT #{columns}, "
908
+ sql << order_columns * ", "
990
909
  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
910
 
911
+ # ORDER BY clause for the passed order option.
912
+ #
913
+ # Uses column aliases as defined by #distinct.
914
+ def add_order_by_for_association_limiting!(sql, options)
915
+ return sql if options[:order].blank?
1011
916
 
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
917
+ order = options[:order].split(',').collect { |s| s.strip }.reject(&:blank?)
918
+ order.map! {|s| $1 if s =~ / (.*)/}
919
+ order = order.zip((0...order.size).to_a).map { |s,i| "alias_#{i}__ #{s}" }.join(', ')
1051
920
 
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
921
+ sql << " ORDER BY #{order}"
1062
922
  end
1063
- end
1064
923
 
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 ]
924
+ private
1070
925
 
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
926
+ def select(sql, name = nil, return_column_names = false)
927
+ log(sql, name) do
928
+ @connection.select(sql, name, return_column_names)
929
+ end
1086
930
  end
1087
- end
1088
931
 
1089
- end
932
+ def oracle_downcase(column_name)
933
+ @connection.oracle_downcase(column_name)
934
+ end
1090
935
 
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
936
+ def column_for(table_name, column_name)
937
+ unless column = columns(table_name).find { |c| c.name == column_name.to_s }
938
+ raise "No such column: #{table_name}.#{column_name}"
939
+ end
940
+ column
1103
941
  end
942
+
1104
943
  end
1105
944
  end
1106
945
  end
@@ -1133,5 +972,14 @@ require 'active_record/connection_adapters/oracle_enhanced_cpk'
1133
972
  # RSI: load patch for dirty tracking methods
1134
973
  require 'active_record/connection_adapters/oracle_enhanced_dirty'
1135
974
 
975
+ # RSI: load rake tasks definitions
976
+ begin
977
+ require 'active_record/connection_adapters/oracle_enhanced_tasks'
978
+ rescue LoadError
979
+ end if defined?(RAILS_ROOT)
980
+
1136
981
  # handles quoting of oracle reserved words
1137
982
  require 'active_record/connection_adapters/oracle_enhanced_reserved_words'
983
+
984
+ # add BigDecimal#to_d, Fixnum#to_d and Bignum#to_d methods if not already present
985
+ require 'active_record/connection_adapters/oracle_enhanced_core_ext'