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.
- data/History.txt +11 -0
- data/README.txt +6 -2
- data/lib/active_record/connection_adapters/emulation/oracle_adapter.rb +5 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +771 -907
- data/lib/active_record/connection_adapters/oracle_enhanced_connection.rb +71 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_core_ext.rb +64 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb +2 -2
- data/lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb +352 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb +346 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_procedures.rb +2 -1
- data/lib/active_record/connection_adapters/oracle_enhanced_reserved_words.rb +126 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_version.rb +2 -2
- data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +200 -97
- data/spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb +170 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_core_ext_spec.rb +40 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb +11 -6
- data/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +148 -53
- data/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb +13 -5
- data/spec/active_record/connection_adapters/oracle_enhanced_emulate_oracle_adapter_spec.rb +27 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb +10 -6
- data/spec/spec_helper.rb +51 -6
- 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.
|
21
|
-
*
|
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:
|
@@ -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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
-
|
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
|
-
|
118
|
-
|
112
|
+
module ConnectionAdapters #:nodoc:
|
113
|
+
class OracleEnhancedColumn < Column #:nodoc:
|
119
114
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
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
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
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
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
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
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
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
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
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
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
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
|
-
|
193
|
-
|
194
|
-
|
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
|
-
|
200
|
-
|
201
|
-
|
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
|
-
|
207
|
-
|
208
|
-
|
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
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
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
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
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
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
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
|
-
|
295
|
-
|
296
|
-
|
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
|
-
|
299
|
-
|
300
|
-
|
295
|
+
def adapter_name #:nodoc:
|
296
|
+
'OracleEnhanced'
|
297
|
+
end
|
301
298
|
|
302
|
-
|
303
|
-
|
304
|
-
|
299
|
+
def supports_migrations? #:nodoc:
|
300
|
+
true
|
301
|
+
end
|
305
302
|
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
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
|
-
|
328
|
-
|
329
|
-
|
324
|
+
def table_alias_length
|
325
|
+
30
|
326
|
+
end
|
330
327
|
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
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
|
-
|
339
|
-
|
340
|
-
|
336
|
+
# QUOTING ==================================================
|
337
|
+
#
|
338
|
+
# see: abstract/quoting.rb
|
341
339
|
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
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
|
-
|
349
|
-
|
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
|
-
|
354
|
-
|
355
|
-
|
360
|
+
end
|
361
|
+
|
362
|
+
def quote_string(s) #:nodoc:
|
363
|
+
s.gsub(/'/, "''")
|
364
|
+
end
|
356
365
|
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
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
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
389
|
+
def quoted_true
|
390
|
+
return "'#{self.class.boolean_to_string(true)}'" if emulate_booleans_from_strings
|
391
|
+
"1"
|
392
|
+
end
|
384
393
|
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
394
|
+
def quoted_false
|
395
|
+
return "'#{self.class.boolean_to_string(false)}'" if emulate_booleans_from_strings
|
396
|
+
"0"
|
397
|
+
end
|
389
398
|
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
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
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
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
|
-
|
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
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
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
|
-
|
424
|
-
|
425
|
-
|
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
|
-
|
431
|
-
|
432
|
-
|
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
|
-
|
435
|
-
|
436
|
-
|
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
|
-
|
448
|
-
|
449
|
-
|
452
|
+
# DATABASE STATEMENTS ======================================
|
453
|
+
#
|
454
|
+
# see: abstract/database_statements.rb
|
450
455
|
|
451
|
-
|
452
|
-
|
453
|
-
|
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
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
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
|
-
|
464
|
-
|
465
|
-
|
467
|
+
def begin_db_transaction #:nodoc:
|
468
|
+
@connection.autocommit = false
|
469
|
+
end
|
466
470
|
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
end
|
473
|
-
end
|
471
|
+
def commit_db_transaction #:nodoc:
|
472
|
+
@connection.commit
|
473
|
+
ensure
|
474
|
+
@connection.autocommit = true
|
475
|
+
end
|
474
476
|
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
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
|
-
|
482
|
-
|
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
|
-
|
487
|
-
|
488
|
-
|
501
|
+
def default_sequence_name(table, column) #:nodoc:
|
502
|
+
quote_table_name("#{table}_seq")
|
503
|
+
end
|
489
504
|
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
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
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
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
|
-
|
508
|
-
end
|
527
|
+
@connection.write_lob(lob, value, col.type == :binary)
|
509
528
|
end
|
510
529
|
end
|
530
|
+
end
|
511
531
|
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
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
|
-
|
534
|
-
|
535
|
-
|
549
|
+
# SCHEMA STATEMENTS ========================================
|
550
|
+
#
|
551
|
+
# see: abstract/schema_statements.rb
|
536
552
|
|
537
|
-
|
538
|
-
|
539
|
-
|
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
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
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
|
-
|
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
|
-
|
598
|
-
|
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
|
-
|
602
|
-
|
603
|
-
|
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
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
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
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
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
|
-
|
649
|
-
|
650
|
-
|
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
|
-
|
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 :
|
670
|
-
def
|
671
|
-
|
672
|
-
|
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
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
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
|
-
|
696
|
-
|
697
|
-
|
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
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
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
|
-
|
707
|
-
|
708
|
-
|
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
|
-
|
711
|
-
|
712
|
-
|
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
|
-
|
715
|
-
|
716
|
-
|
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
|
-
|
721
|
-
|
722
|
-
|
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
|
-
|
725
|
-
|
726
|
-
|
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
|
-
|
729
|
-
|
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
|
-
|
735
|
-
|
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
|
-
|
740
|
-
|
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
|
-
|
749
|
-
|
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
|
-
#
|
759
|
-
#
|
760
|
-
|
761
|
-
|
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
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
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
|
-
|
784
|
-
|
785
|
-
|
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
|
-
|
812
|
-
|
813
|
-
|
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
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
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
|
-
|
823
|
-
|
824
|
-
|
825
|
-
|
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
|
-
|
831
|
-
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
|
836
|
-
|
837
|
-
|
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
|
-
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
|
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
|
-
|
860
|
-
|
861
|
-
|
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
|
-
|
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
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
|
879
|
-
|
880
|
-
|
881
|
-
|
882
|
-
|
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
|
-
|
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
|
-
|
916
|
-
|
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
|
-
|
921
|
-
|
922
|
-
|
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
|
-
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
|
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
|
-
|
876
|
+
# from abstract adapter
|
877
|
+
sql << " DEFAULT #{quote(options[:default], options[:column])}"
|
950
878
|
end
|
951
|
-
|
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
|
-
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
|
963
|
-
|
964
|
-
|
965
|
-
|
966
|
-
|
967
|
-
|
968
|
-
|
969
|
-
|
970
|
-
|
971
|
-
|
972
|
-
|
973
|
-
|
974
|
-
|
975
|
-
|
976
|
-
|
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
|
-
|
1001
|
-
|
1002
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
1060
|
-
|
1061
|
-
|
1062
|
-
|
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
|
-
|
932
|
+
def oracle_downcase(column_name)
|
933
|
+
@connection.oracle_downcase(column_name)
|
934
|
+
end
|
1078
935
|
|
1079
|
-
|
1080
|
-
|
1081
|
-
|
1082
|
-
|
1083
|
-
|
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
|
+
|