activerecord-oracle_enhanced-adapter 1.1.9 → 1.2.0

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