plukevdh-activerecord-oracle_enhanced-adapter 1.2.1

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 (29) hide show
  1. data/History.txt +111 -0
  2. data/License.txt +20 -0
  3. data/Manifest.txt +26 -0
  4. data/README.rdoc +68 -0
  5. data/lib/active_record/connection_adapters/emulation/oracle_adapter.rb +5 -0
  6. data/lib/active_record/connection_adapters/oracle_enhanced.rake +48 -0
  7. data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +1200 -0
  8. data/lib/active_record/connection_adapters/oracle_enhanced_connection.rb +71 -0
  9. data/lib/active_record/connection_adapters/oracle_enhanced_core_ext.rb +64 -0
  10. data/lib/active_record/connection_adapters/oracle_enhanced_cpk.rb +21 -0
  11. data/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb +39 -0
  12. data/lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb +358 -0
  13. data/lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb +368 -0
  14. data/lib/active_record/connection_adapters/oracle_enhanced_procedures.rb +150 -0
  15. data/lib/active_record/connection_adapters/oracle_enhanced_reserved_words.rb +126 -0
  16. data/lib/active_record/connection_adapters/oracle_enhanced_tasks.rb +11 -0
  17. data/lib/active_record/connection_adapters/oracle_enhanced_version.rb +7 -0
  18. data/oracle-enhanced.gemspec +59 -0
  19. data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +659 -0
  20. data/spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb +170 -0
  21. data/spec/active_record/connection_adapters/oracle_enhanced_core_ext_spec.rb +40 -0
  22. data/spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb +103 -0
  23. data/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +951 -0
  24. data/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb +93 -0
  25. data/spec/active_record/connection_adapters/oracle_enhanced_emulate_oracle_adapter_spec.rb +27 -0
  26. data/spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb +340 -0
  27. data/spec/spec.opts +6 -0
  28. data/spec/spec_helper.rb +94 -0
  29. metadata +94 -0
@@ -0,0 +1,111 @@
1
+ == 1.2.1 2009-06-07
2
+
3
+ * Enhancements
4
+ * caching of table indexes query which makes schema dump much faster
5
+ * Bug fixes:
6
+ * return Date (and not DateTime) values for :date column value before year 1970
7
+ * fixed after_create/update/destroy callbacks with plsql custom methods
8
+ * fixed creation of large integers in JRuby
9
+ * Made test tasks respect RAILS_ENV
10
+ * fixed support for composite primary keys for tables with LOBs
11
+
12
+ == 1.2.0 2009-03-22
13
+
14
+ * Enhancements
15
+ * support for JRuby and JDBC
16
+ * support for Ruby 1.9.1 and ruby-oci8 2.0
17
+ * support for Rails 2.3
18
+ * quoting of Oracle reserved words in table names and column names
19
+ * emulation of OracleAdapter (for ActiveRecord unit tests)
20
+ * Bug fixes:
21
+ * several bug fixes that were identified during running of ActiveRecord unit tests
22
+
23
+ == 1.1.9 2009-01-02
24
+
25
+ * Enhancements
26
+ * Added support for table and column comments in migrations
27
+ * Added support for specifying sequence start values
28
+ * Added :privilege option (e.g. :SYSDBA) to ActiveRecord::Base.establish_connection
29
+ * Bug fixes:
30
+ * Do not mark empty decimals, strings and texts (stored as NULL in database) as changed when reassigning them (starting from Rails 2.1)
31
+ * Create booleans as VARCHAR2(1) columns if emulate_booleans_from_strings is true
32
+
33
+ == 1.1.8 2008-10-10
34
+
35
+ * Bug fixes:
36
+ * Fixed storing of serialized LOB columns
37
+ * Prevent from SQL injection in :limit and :offset
38
+ * Order by LOB columns (by replacing column with function which returns first 100 characters of LOB)
39
+ * Sequence creation for tables with non-default primary key in create_table block
40
+ * Do count distinct workaround only when composite_primary_keys gem is used
41
+ (otherwise count distinct did not work with ActiveRecord 2.1.1)
42
+ * Fixed rake db:test:clone_structure task
43
+ (see http://rsim.lighthouseapp.com/projects/11468/tickets/11-rake-dbtestclone_structure-fails-in-117)
44
+ * Fixed bug when ActiveRecord::Base.allow_concurrency = true
45
+ (see http://dev.rubyonrails.org/ticket/11134)
46
+
47
+ == 1.1.7 2008-08-20
48
+
49
+ * Bug fixes:
50
+ * Fixed that adapter works without ruby-plsql gem (in this case just custom create/update/delete methods are not available)
51
+
52
+ == 1.1.6 2008-08-19
53
+
54
+ * Enhancements:
55
+ * Added support for set_date_columns and set_datetime_columns
56
+ * Added support for set_boolean_columns
57
+ * Added support for schema prefix in set_table_name (removed table name quoting)
58
+ * Added support for NVARCHAR2 column type
59
+ * Bug fixes:
60
+ * Do not call write_lobs callback when custom create or update methods are defined
61
+
62
+ == 1.1.5 2008-07-27
63
+
64
+ * Bug fixes:
65
+ * Fixed that write_lobs callback works with partial_updates enabled (added additional record lock before writing BLOB data to database)
66
+ * Enhancements:
67
+ * Changed SQL SELECT in indexes method so that it will execute faster on some large data dictionaries
68
+ * Support for other date and time formats when assigning string to :date or :datetime column
69
+
70
+ == 1.1.4 2008-07-14
71
+
72
+ * Enhancements:
73
+ * Date/Time quoting changes to support composite_primary_keys
74
+ * Added additional methods that are used by composite_primary_keys
75
+
76
+ == 1.1.3 2008-07-10
77
+
78
+ * Enhancements:
79
+ * Added support for custom create, update and delete methods when working with legacy databases where
80
+ PL/SQL API should be used for create, update and delete operations
81
+
82
+ == 1.1.2 2008-07-08
83
+
84
+ * Bug fixes:
85
+ * Fixed after_save callback addition for session store in ActiveRecord version 2.0.2
86
+ * Changed date column name recognition - now should match regex /(^|_)date(_|$)/i
87
+ (previously "updated_at" was recognized as :date column and not as :datetime)
88
+
89
+ == 1.1.1 2008-06-28
90
+
91
+ * Enhancements:
92
+ * Added ignore_table_columns option
93
+ * Added support for TIMESTAMP columns (without fractional seconds)
94
+ * NLS_DATE_FORMAT and NLS_TIMESTAMP_FORMAT independent DATE and TIMESTAMP columns support
95
+ * Bug fixes:
96
+ * Checks if CGI::Session::ActiveRecordStore::Session does not have enhanced_write_lobs callback before adding it
97
+ (Rails 2.0 does not add this callback, Rails 2.1 does)
98
+
99
+ == 1.1.0 2008-05-05
100
+
101
+ * Forked from original activerecord-oracle-adapter-1.0.0.9216
102
+ * Renamed oracle adapter to oracle_enhanced adapter
103
+ * Added "enhanced" to method and class definitions so that oracle_enhanced and original oracle adapter
104
+ could be used simultaniously
105
+ * Added Rails rake tasks as a copy from original oracle tasks
106
+ * Enhancements:
107
+ * Improved perfomance of schema dump methods when used on large data dictionaries
108
+ * Added LOB writing callback for sessions stored in database
109
+ * Added emulate_dates_by_column_name option
110
+ * Added emulate_integers_by_column_name option
111
+ * Added emulate_booleans_from_strings option
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Graham Jenkins, Michael Schoen, Raimonds Simanovskis
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,26 @@
1
+ History.txt
2
+ License.txt
3
+ README.rdoc
4
+ lib/active_record/connection_adapters/emulation/oracle_adapter.rb
5
+ lib/active_record/connection_adapters/oracle_enhanced.rake
6
+ lib/active_record/connection_adapters/oracle_enhanced_adapter.rb
7
+ lib/active_record/connection_adapters/oracle_enhanced_connection.rb
8
+ lib/active_record/connection_adapters/oracle_enhanced_core_ext.rb
9
+ lib/active_record/connection_adapters/oracle_enhanced_cpk.rb
10
+ lib/active_record/connection_adapters/oracle_enhanced_dirty.rb
11
+ lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb
12
+ lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb
13
+ lib/active_record/connection_adapters/oracle_enhanced_procedures.rb
14
+ lib/active_record/connection_adapters/oracle_enhanced_reserved_words.rb
15
+ lib/active_record/connection_adapters/oracle_enhanced_tasks.rb
16
+ lib/active_record/connection_adapters/oracle_enhanced_version.rb
17
+ spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb
18
+ spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb
19
+ spec/active_record/connection_adapters/oracle_enhanced_core_ext_spec.rb
20
+ spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb
21
+ spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb
22
+ spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb
23
+ spec/active_record/connection_adapters/oracle_enhanced_emulate_oracle_adapter_spec.rb
24
+ spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb
25
+ spec/spec.opts
26
+ spec/spec_helper.rb
@@ -0,0 +1,68 @@
1
+ = Activerecord Oracle enhanced adapter
2
+
3
+ * http://github.com/rsim/oracle-enhanced
4
+
5
+ == DESCRIPTION:
6
+
7
+ Oracle "enhanced" ActiveRecord adapter contains useful additional methods for working with new and legacy Oracle databases
8
+ from Rails which are extracted from current real projects' monkey patches of original Oracle adapter.
9
+
10
+ See http://github.com/rsim/oracle-enhanced/wikis for usage information.
11
+
12
+ For questions and feature discussion please use http://groups.google.com/group/oracle-enhanced
13
+
14
+ Blog posts about oracle-enahnced can be found at http://blog.rayapps.com/category/oracle-enhanced
15
+
16
+ Bugs and enhancement requests can be reported at http://rsim.lighthouseapp.com/projects/11468-oracle-enhanced
17
+
18
+ == REQUIREMENTS:
19
+
20
+ * Works (has been tested) with ActiveRecord version 2.0, 2.1, 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 Java class path)
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)
27
+
28
+ == INSTALL:
29
+
30
+ * [sudo] gem install activerecord-oracle_enhanced-adapter
31
+
32
+ == CONTRIBUTORS:
33
+
34
+ * Raimonds Simanovskis
35
+ * Jorge Dias
36
+ * James Wylder
37
+ * Rob Christie
38
+ * Nate Wieger
39
+ * Edgars Beigarts
40
+ * Lachlan Laycock
41
+ * toddwf
42
+ * Anton Jenkins
43
+ * Dave Smylie
44
+
45
+ == LICENSE:
46
+
47
+ (The MIT License)
48
+
49
+ Copyright (c) 2009 Graham Jenkins, Michael Schoen, Raimonds Simanovskis
50
+
51
+ Permission is hereby granted, free of charge, to any person obtaining
52
+ a copy of this software and associated documentation files (the
53
+ 'Software'), to deal in the Software without restriction, including
54
+ without limitation the rights to use, copy, modify, merge, publish,
55
+ distribute, sublicense, and/or sell copies of the Software, and to
56
+ permit persons to whom the Software is furnished to do so, subject to
57
+ the following conditions:
58
+
59
+ The above copyright notice and this permission notice shall be
60
+ included in all copies or substantial portions of the Software.
61
+
62
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
63
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
64
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
65
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
66
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
67
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
68
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,5 @@
1
+ class ActiveRecord::ConnectionAdapters::OracleAdapter < ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter #:nodoc:
2
+ def adapter_name
3
+ 'Oracle'
4
+ end
5
+ end
@@ -0,0 +1,48 @@
1
+ # RSI: implementation idea taken from JDBC adapter
2
+ def redefine_task(*args, &block)
3
+ task_name = Hash === args.first ? args.first.keys[0] : args.first
4
+ existing_task = Rake.application.lookup task_name
5
+ if existing_task
6
+ class << existing_task; public :instance_variable_set; end
7
+ existing_task.instance_variable_set "@prerequisites", FileList[]
8
+ existing_task.instance_variable_set "@actions", []
9
+ end
10
+ task(*args, &block)
11
+ end
12
+
13
+ namespace :db do
14
+
15
+ namespace :structure do
16
+ redefine_task :dump => :environment do
17
+ abcs = ActiveRecord::Base.configurations
18
+ ActiveRecord::Base.establish_connection(abcs[RAILS_ENV])
19
+ File.open("db/#{RAILS_ENV}_structure.sql", "w+") { |f| f << ActiveRecord::Base.connection.structure_dump }
20
+ if ActiveRecord::Base.connection.supports_migrations?
21
+ File.open("db/#{RAILS_ENV}_structure.sql", "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information }
22
+ end
23
+ if abcs[RAILS_ENV]['structure_dump'] == "db_stored_code"
24
+ File.open("db/#{RAILS_ENV}_structure.sql", "a") { |f| f << ActiveRecord::Base.connection.structure_dump_db_stored_code }
25
+ end
26
+
27
+ end
28
+ end
29
+
30
+ namespace :test do
31
+ redefine_task :clone_structure => [ "db:structure:dump", "db:test:purge" ] do
32
+ abcs = ActiveRecord::Base.configurations
33
+ ActiveRecord::Base.establish_connection(:test)
34
+ IO.readlines("db/#{RAILS_ENV}_structure.sql").join.split("\n\n").each do |ddl|
35
+ ActiveRecord::Base.connection.execute(ddl.chop)
36
+ end
37
+ end
38
+
39
+ redefine_task :purge => :environment do
40
+ abcs = ActiveRecord::Base.configurations
41
+ ActiveRecord::Base.establish_connection(:test)
42
+ ActiveRecord::Base.connection.structure_drop.split("\n\n").each do |ddl|
43
+ ActiveRecord::Base.connection.execute(ddl.chop)
44
+ end
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,1200 @@
1
+ # oracle_enhanced_adapter.rb -- ActiveRecord adapter for Oracle 8i, 9i, 10g, 11g
2
+ #
3
+ # Authors or original oracle_adapter: Graham Jenkins, Michael Schoen
4
+ #
5
+ # Current maintainer: Raimonds Simanovskis (http://blog.rayapps.com)
6
+ #
7
+ #########################################################################
8
+ #
9
+ # See History.txt for changes added to original oracle_adapter.rb
10
+ #
11
+ #########################################################################
12
+ #
13
+ # From original oracle_adapter.rb:
14
+ #
15
+ # Implementation notes:
16
+ # 1. Redefines (safely) a method in ActiveRecord to make it possible to
17
+ # implement an autonumbering solution for Oracle.
18
+ # 2. The OCI8 driver is patched to properly handle values for LONG and
19
+ # TIMESTAMP columns. The driver-author has indicated that a future
20
+ # release of the driver will obviate this patch.
21
+ # 3. LOB support is implemented through an after_save callback.
22
+ # 4. Oracle does not offer native LIMIT and OFFSET options; this
23
+ # functionality is mimiced through the use of nested selects.
24
+ # See http://asktom.oracle.com/pls/ask/f?p=4950:8:::::F4950_P8_DISPLAYID:127412348064
25
+ #
26
+ # Do what you want with this code, at your own peril, but if any
27
+ # significant portion of my code remains then please acknowledge my
28
+ # contribution.
29
+ # portions Copyright 2005 Graham Jenkins
30
+
31
+ require 'active_record/connection_adapters/abstract_adapter'
32
+
33
+ require 'active_record/connection_adapters/oracle_enhanced_connection'
34
+
35
+ module ActiveRecord
36
+ class Base
37
+ # Establishes a connection to the database that's used by all Active Record objects.
38
+ def self.oracle_enhanced_connection(config) #:nodoc:
39
+ if config[:emulate_oracle_adapter] == true
40
+ # allows the enhanced adapter to look like the OracleAdapter. Useful to pick up
41
+ # conditionals in the rails activerecord test suite
42
+ require 'active_record/connection_adapters/emulation/oracle_adapter'
43
+ ConnectionAdapters::OracleAdapter.new(
44
+ ConnectionAdapters::OracleEnhancedConnection.create(config), logger)
45
+ else
46
+ ConnectionAdapters::OracleEnhancedAdapter.new(
47
+ ConnectionAdapters::OracleEnhancedConnection.create(config), logger)
48
+ end
49
+ end
50
+
51
+ # Specify table columns which should be ignored by ActiveRecord.
52
+ def self.ignore_table_columns(*args)
53
+ connection.ignore_table_columns(table_name,*args)
54
+ end
55
+
56
+ # Specify which table columns should be typecasted to Date (without time).
57
+ def self.set_date_columns(*args)
58
+ connection.set_type_for_columns(table_name,:date,*args)
59
+ end
60
+
61
+ # Specify which table columns should be typecasted to Time (or DateTime).
62
+ def self.set_datetime_columns(*args)
63
+ connection.set_type_for_columns(table_name,:datetime,*args)
64
+ end
65
+
66
+ # Specify which table columns should be typecasted to boolean values +true+ or +false+.
67
+ def self.set_boolean_columns(*args)
68
+ connection.set_type_for_columns(table_name,:boolean,*args)
69
+ end
70
+
71
+ # After setting large objects to empty, select the OCI8::LOB
72
+ # and write back the data.
73
+ after_save :enhanced_write_lobs
74
+ def enhanced_write_lobs #:nodoc:
75
+ if connection.is_a?(ConnectionAdapters::OracleEnhancedAdapter) &&
76
+ !(self.class.custom_create_method || self.class.custom_update_method)
77
+ connection.write_lobs(self.class.table_name, self.class, attributes)
78
+ end
79
+ end
80
+ private :enhanced_write_lobs
81
+
82
+ class << self
83
+ # patch ORDER BY to work with LOBs
84
+ def add_order_with_lobs!(sql, order, scope = :auto)
85
+ if connection.is_a?(ConnectionAdapters::OracleEnhancedAdapter)
86
+ order = connection.lob_order_by_expression(self, order) if order
87
+
88
+ orig_scope = scope
89
+ scope = scope(:find) if :auto == scope
90
+ if scope
91
+ new_scope_order = connection.lob_order_by_expression(self, scope[:order])
92
+ if new_scope_order != scope[:order]
93
+ scope = scope.merge(:order => new_scope_order)
94
+ else
95
+ scope = orig_scope
96
+ end
97
+ end
98
+ end
99
+ add_order_without_lobs!(sql, order, scope = :auto)
100
+ end
101
+ private :add_order_with_lobs!
102
+ #:stopdoc:
103
+ alias_method :add_order_without_lobs!, :add_order!
104
+ alias_method :add_order!, :add_order_with_lobs!
105
+ #:startdoc:
106
+ end
107
+
108
+ # Get table comment from schema definition.
109
+ def self.table_comment
110
+ connection.table_comment(self.table_name)
111
+ end
112
+ end
113
+
114
+
115
+ module ConnectionAdapters #:nodoc:
116
+ class OracleEnhancedColumn < Column
117
+
118
+ attr_reader :table_name, :forced_column_type #:nodoc:
119
+
120
+ def initialize(name, default, sql_type = nil, null = true, table_name = nil, forced_column_type = nil) #:nodoc:
121
+ @table_name = table_name
122
+ @forced_column_type = forced_column_type
123
+ super(name, default, sql_type, null)
124
+ end
125
+
126
+ def type_cast(value) #:nodoc:
127
+ return guess_date_or_time(value) if type == :datetime && OracleEnhancedAdapter.emulate_dates
128
+ super
129
+ end
130
+
131
+ # convert something to a boolean
132
+ # added y as boolean value
133
+ def self.value_to_boolean(value) #:nodoc:
134
+ if value == true || value == false
135
+ value
136
+ elsif value.is_a?(String) && value.blank?
137
+ nil
138
+ else
139
+ %w(true t 1 y +).include?(value.to_s.downcase)
140
+ end
141
+ end
142
+
143
+ # convert Time or DateTime value to Date for :date columns
144
+ def self.string_to_date(string) #:nodoc:
145
+ return string.to_date if string.is_a?(Time) || string.is_a?(DateTime)
146
+ super
147
+ end
148
+
149
+ # convert Date value to Time for :datetime columns
150
+ def self.string_to_time(string) #:nodoc:
151
+ return string.to_time if string.is_a?(Date) && !OracleEnhancedAdapter.emulate_dates
152
+ super
153
+ end
154
+
155
+ # 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
160
+
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
+ # 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
182
+ end
183
+
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
191
+
192
+ def fallback_string_to_date(string) #:nodoc:
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
197
+ end
198
+
199
+ def fallback_string_to_time(string) #:nodoc:
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
204
+ end
205
+
206
+ def string_to_date_or_time_using_format(string) #:nodoc:
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)
211
+ end
212
+
213
+ end
214
+ end
215
+
216
+
217
+ # Oracle enhanced adapter will work with both
218
+ # Ruby 1.8/1.9 ruby-oci8 gem (which provides interface to Oracle OCI client)
219
+ # or with JRuby and Oracle JDBC driver.
220
+ #
221
+ # It should work with Oracle 9i, 10g and 11g databases.
222
+ # Limited set of functionality should work on Oracle 8i as well but several features
223
+ # rely on newer functionality in Oracle database.
224
+ #
225
+ # Usage notes:
226
+ # * Key generation assumes a "${table_name}_seq" sequence is available
227
+ # for all tables; the sequence name can be changed using
228
+ # ActiveRecord::Base.set_sequence_name. When using Migrations, these
229
+ # sequences are created automatically.
230
+ # * Oracle uses DATE or TIMESTAMP datatypes for both dates and times.
231
+ # Consequently some hacks are employed to map data back to Date or Time
232
+ # in Ruby. 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
+ #
238
+ # Required parameters:
239
+ #
240
+ # * <tt>:username</tt>
241
+ # * <tt>:password</tt>
242
+ # * <tt>:database</tt> - either TNS alias or connection string for OCI client or database name in JDBC connection string
243
+ #
244
+ # Optional parameters:
245
+ #
246
+ # * <tt>:host</tt> - host name for JDBC connection, defaults to "localhost"
247
+ # * <tt>:port</tt> - port number for JDBC connection, defaults to 1521
248
+ # * <tt>:privilege</tt> - set "SYSDBA" if you want to connect with this privilege
249
+ # * <tt>:allow_concurrency</tt> - set to "true" if non-blocking mode should be enabled (just for OCI client)
250
+ # * <tt>:prefetch_rows</tt> - how many rows should be fetched at one time to increase performance, defaults to 100
251
+ # * <tt>:cursor_sharing</tt> - cursor sharing mode to minimize amount of unique statements, defaults to "similar"
252
+ # * <tt>:nls_length_semantics</tt> - semantics of size of VARCHAR2 and CHAR columns, defaults to "CHAR"
253
+ # (meaning that size specifies number of characters and not bytes)
254
+ class OracleEnhancedAdapter < AbstractAdapter
255
+
256
+ ##
257
+ # :singleton-method:
258
+ # By default, the OracleEnhancedAdapter will consider all columns of type <tt>NUMBER(1)</tt>
259
+ # as boolean. If you wish to disable this emulation you can add the following line
260
+ # to your initializer file:
261
+ #
262
+ # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_booleans = false
263
+ cattr_accessor :emulate_booleans
264
+ self.emulate_booleans = true
265
+
266
+ ##
267
+ # :singleton-method:
268
+ # By default, the OracleEnhancedAdapter will typecast all columns of type <tt>DATE</tt>
269
+ # to Time or DateTime (if value is out of Time value range) value.
270
+ # If you wish that DATE values with hour, minutes and seconds equal to 0 are typecasted
271
+ # to Date then you can add the following line to your initializer file:
272
+ #
273
+ # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_dates = true
274
+ #
275
+ # As this option can have side effects when unnecessary typecasting is done it is recommended
276
+ # that Date columns are explicily defined with +set_date_columns+ method.
277
+ cattr_accessor :emulate_dates
278
+ self.emulate_dates = false
279
+
280
+ ##
281
+ # :singleton-method:
282
+ # By default, the OracleEnhancedAdapter will typecast all columns of type <tt>DATE</tt>
283
+ # to Time or DateTime (if value is out of Time value range) value.
284
+ # If you wish that DATE columns with "date" in their name (e.g. "creation_date") are typecasted
285
+ # to Date then you can add the following line to your initializer file:
286
+ #
287
+ # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_dates_by_column_name = true
288
+ #
289
+ # As this option can have side effects when unnecessary typecasting is done it is recommended
290
+ # that Date columns are explicily defined with +set_date_columns+ method.
291
+ cattr_accessor :emulate_dates_by_column_name
292
+ self.emulate_dates_by_column_name = false
293
+
294
+ # Check column name to identify if it is Date (and not Time) column.
295
+ # Is used if +emulate_dates_by_column_name+ option is set to +true+.
296
+ # Override this method definition in initializer file if different Date column recognition is needed.
297
+ def self.is_date_column?(name, table_name = nil)
298
+ name =~ /(^|_)date(_|$)/i
299
+ end
300
+
301
+ # instance method uses at first check if column type defined at class level
302
+ def is_date_column?(name, table_name = nil) #:nodoc:
303
+ case get_type_for_column(table_name, name)
304
+ when nil
305
+ self.class.is_date_column?(name, table_name)
306
+ when :date
307
+ true
308
+ else
309
+ false
310
+ end
311
+ end
312
+
313
+ ##
314
+ # :singleton-method:
315
+ # By default, the OracleEnhancedAdapter will typecast all columns of type <tt>NUMBER</tt>
316
+ # (without precision or scale) to Float or BigDecimal value.
317
+ # If you wish that NUMBER columns with name "id" or that end with "_id" are typecasted
318
+ # to Integer then you can add the following line to your initializer file:
319
+ #
320
+ # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_integers_by_column_name = true
321
+ cattr_accessor :emulate_integers_by_column_name
322
+ self.emulate_integers_by_column_name = false
323
+
324
+ # Check column name to identify if it is Integer (and not Float or BigDecimal) column.
325
+ # Is used if +emulate_integers_by_column_name+ option is set to +true+.
326
+ # Override this method definition in initializer file if different Integer column recognition is needed.
327
+ def self.is_integer_column?(name, table_name = nil)
328
+ name =~ /((^|_)id|_(pk|fk))$/i
329
+ end
330
+
331
+ ##
332
+ # :singleton-method:
333
+ # If you wish that CHAR(1), VARCHAR2(1) columns or VARCHAR2 columns with FLAG or YN at the end of their name
334
+ # are typecasted to booleans then you can add the following line to your initializer file:
335
+ #
336
+ # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_booleans_from_strings = true
337
+ cattr_accessor :emulate_booleans_from_strings
338
+ self.emulate_booleans_from_strings = false
339
+
340
+ # Check column name to identify if it is boolean (and not String) column.
341
+ # Is used if +emulate_booleans_from_strings+ option is set to +true+.
342
+ # Override this method definition in initializer file if different boolean column recognition is needed.
343
+ def self.is_boolean_column?(name, field_type, table_name = nil)
344
+ return true if ["CHAR(1)","VARCHAR2(1)"].include?(field_type)
345
+ field_type =~ /^VARCHAR2/ && (name =~ /_flag$/i || name =~ /_yn$/i)
346
+ end
347
+
348
+ # How boolean value should be quoted to String.
349
+ # Used if +emulate_booleans_from_strings+ option is set to +true+.
350
+ def self.boolean_to_string(bool)
351
+ bool ? "Y" : "N"
352
+ end
353
+
354
+ # Specify non-default date format that should be used when assigning string values to :date columns, e.g.:
355
+ #
356
+ # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.string_to_date_format = “%d.%m.%Y”
357
+ cattr_accessor :string_to_date_format
358
+ self.string_to_date_format = nil
359
+
360
+ # Specify non-default time format that should be used when assigning string values to :datetime columns, e.g.:
361
+ #
362
+ # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.string_to_time_format = “%d.%m.%Y %H:%M:%S”
363
+ cattr_accessor :string_to_time_format
364
+ self.string_to_time_format = nil
365
+
366
+ def initialize(connection, logger = nil) #:nodoc:
367
+ super
368
+ @quoted_column_names, @quoted_table_names = {}, {}
369
+ end
370
+
371
+ def adapter_name #:nodoc:
372
+ 'OracleEnhanced'
373
+ end
374
+
375
+ def supports_migrations? #:nodoc:
376
+ true
377
+ end
378
+
379
+ def native_database_types #:nodoc:
380
+ {
381
+ :primary_key => "NUMBER(38) NOT NULL PRIMARY KEY",
382
+ :string => { :name => "VARCHAR2", :limit => 255 },
383
+ :text => { :name => "CLOB" },
384
+ :integer => { :name => "NUMBER", :limit => 38 },
385
+ :float => { :name => "NUMBER" },
386
+ :decimal => { :name => "DECIMAL" },
387
+ :datetime => { :name => "DATE" },
388
+ # changed to native TIMESTAMP type
389
+ # :timestamp => { :name => "DATE" },
390
+ :timestamp => { :name => "TIMESTAMP" },
391
+ :time => { :name => "DATE" },
392
+ :date => { :name => "DATE" },
393
+ :binary => { :name => "BLOB" },
394
+ # if emulate_booleans_from_strings then store booleans in VARCHAR2
395
+ :boolean => emulate_booleans_from_strings ?
396
+ { :name => "VARCHAR2", :limit => 1 } : { :name => "NUMBER", :limit => 1 }
397
+ }
398
+ end
399
+
400
+ # maximum length of Oracle identifiers
401
+ IDENTIFIER_MAX_LENGTH = 30
402
+
403
+ def table_alias_length #:nodoc:
404
+ IDENTIFIER_MAX_LENGTH
405
+ end
406
+
407
+ # QUOTING ==================================================
408
+ #
409
+ # see: abstract/quoting.rb
410
+
411
+ def quote_column_name(name) #:nodoc:
412
+ # camelCase column names need to be quoted; not that anyone using Oracle
413
+ # would really do this, but handling this case means we pass the test...
414
+ @quoted_column_names[name] = name.to_s =~ /[A-Z]/ ? "\"#{name}\"" : quote_oracle_reserved_words(name)
415
+ end
416
+
417
+ # unescaped table name should start with letter and
418
+ # contain letters, digits, _, $ or #
419
+ # can be prefixed with schema name
420
+ # CamelCase table names should be quoted
421
+ def self.valid_table_name?(name) #:nodoc:
422
+ name = name.to_s
423
+ name =~ /^([A-Za-z_0-9]+\.)?[a-z][a-z_0-9\$#]*$/ ||
424
+ name =~ /^([A-Za-z_0-9]+\.)?[A-Z][A-Z_0-9\$#]*$/ ? true : false
425
+ end
426
+
427
+ def quote_table_name(name) #:nodoc:
428
+ # abstract_adapter calls quote_column_name from quote_table_name, so prevent that
429
+ @quoted_table_names[name] ||= if self.class.valid_table_name?(name)
430
+ name
431
+ else
432
+ "\"#{name}\""
433
+ end
434
+ end
435
+
436
+ def quote_string(s) #:nodoc:
437
+ s.gsub(/'/, "''")
438
+ end
439
+
440
+ def quote(value, column = nil) #:nodoc:
441
+ if value && column
442
+ case column.type
443
+ when :text, :binary
444
+ %Q{empty_#{ column.sql_type.downcase rescue 'blob' }()}
445
+ # NLS_DATE_FORMAT independent TIMESTAMP support
446
+ when :timestamp
447
+ quote_timestamp_with_to_timestamp(value)
448
+ # NLS_DATE_FORMAT independent DATE support
449
+ when :date, :time, :datetime
450
+ quote_date_with_to_date(value)
451
+ else
452
+ super
453
+ end
454
+ elsif value.acts_like?(:date)
455
+ quote_date_with_to_date(value)
456
+ elsif value.acts_like?(:time)
457
+ value.to_i == value.to_f ? quote_date_with_to_date(value) : quote_timestamp_with_to_timestamp(value)
458
+ else
459
+ super
460
+ end
461
+ end
462
+
463
+ def quoted_true #:nodoc:
464
+ return "'#{self.class.boolean_to_string(true)}'" if emulate_booleans_from_strings
465
+ "1"
466
+ end
467
+
468
+ def quoted_false #:nodoc:
469
+ return "'#{self.class.boolean_to_string(false)}'" if emulate_booleans_from_strings
470
+ "0"
471
+ end
472
+
473
+ def quote_date_with_to_date(value) #:nodoc:
474
+ # should support that composite_primary_keys gem will pass date as string
475
+ value = quoted_date(value) if value.acts_like?(:date) || value.acts_like?(:time)
476
+ "TO_DATE('#{value}','YYYY-MM-DD HH24:MI:SS')"
477
+ end
478
+
479
+ def quote_timestamp_with_to_timestamp(value) #:nodoc:
480
+ # add up to 9 digits of fractional seconds to inserted time
481
+ value = "#{quoted_date(value)}.#{("%.6f"%value.to_f).split('.')[1]}" if value.acts_like?(:time)
482
+ "TO_TIMESTAMP('#{value}','YYYY-MM-DD HH24:MI:SS.FF6')"
483
+ end
484
+
485
+ # CONNECTION MANAGEMENT ====================================
486
+ #
487
+
488
+ # If SQL statement fails due to lost connection then reconnect
489
+ # and retry SQL statement if autocommit mode is enabled.
490
+ # By default this functionality is disabled.
491
+ attr_reader :auto_retry #:nodoc:
492
+ @auto_retry = false
493
+
494
+ def auto_retry=(value) #:nodoc:
495
+ @auto_retry = value
496
+ @connection.auto_retry = value if @connection
497
+ end
498
+
499
+ # return raw OCI8 or JDBC connection
500
+ def raw_connection
501
+ @connection.raw_connection
502
+ end
503
+
504
+ # Returns true if the connection is active.
505
+ def active? #:nodoc:
506
+ # Pings the connection to check if it's still good. Note that an
507
+ # #active? method is also available, but that simply returns the
508
+ # last known state, which isn't good enough if the connection has
509
+ # gone stale since the last use.
510
+ @connection.ping
511
+ rescue OracleEnhancedConnectionException
512
+ false
513
+ end
514
+
515
+ # Reconnects to the database.
516
+ def reconnect! #:nodoc:
517
+ @connection.reset!
518
+ rescue OracleEnhancedConnectionException => e
519
+ @logger.warn "#{adapter_name} automatic reconnection failed: #{e.message}" if @logger
520
+ end
521
+
522
+ # Disconnects from the database.
523
+ def disconnect! #:nodoc:
524
+ @connection.logoff rescue nil
525
+ end
526
+
527
+
528
+ # DATABASE STATEMENTS ======================================
529
+ #
530
+ # see: abstract/database_statements.rb
531
+
532
+ # Executes a SQL statement
533
+ def execute(sql, name = nil)
534
+ log(sql, name) { @connection.exec sql }
535
+ end
536
+
537
+ # Returns an array of arrays containing the field values.
538
+ # Order is the same as that returned by #columns.
539
+ def select_rows(sql, name = nil)
540
+ # last parameter indicates to return also column list
541
+ result, columns = select(sql, name, true)
542
+ result.map{ |v| columns.map{|c| v[c]} }
543
+ end
544
+
545
+ # Returns the next sequence value from a sequence generator. Not generally
546
+ # called directly; used by ActiveRecord to get the next primary key value
547
+ # when inserting a new database record (see #prefetch_primary_key?).
548
+ def next_sequence_value(sequence_name)
549
+ select_one("select #{sequence_name}.nextval id from dual")['id']
550
+ end
551
+
552
+ def begin_db_transaction #:nodoc:
553
+ @connection.autocommit = false
554
+ end
555
+
556
+ def commit_db_transaction #:nodoc:
557
+ @connection.commit
558
+ ensure
559
+ @connection.autocommit = true
560
+ end
561
+
562
+ def rollback_db_transaction #:nodoc:
563
+ @connection.rollback
564
+ ensure
565
+ @connection.autocommit = true
566
+ end
567
+
568
+ def add_limit_offset!(sql, options) #:nodoc:
569
+ # added to_i for limit and offset to protect from SQL injection
570
+ offset = (options[:offset] || 0).to_i
571
+
572
+ if limit = options[:limit]
573
+ limit = limit.to_i
574
+ sql.replace "select * from (select raw_sql_.*, rownum raw_rnum_ from (#{sql}) raw_sql_ where rownum <= #{offset+limit}) where raw_rnum_ > #{offset}"
575
+ elsif offset > 0
576
+ sql.replace "select * from (select raw_sql_.*, rownum raw_rnum_ from (#{sql}) raw_sql_) where raw_rnum_ > #{offset}"
577
+ end
578
+ end
579
+
580
+ # Returns true for Oracle adapter (since Oracle requires primary key
581
+ # values to be pre-fetched before insert). See also #next_sequence_value.
582
+ def prefetch_primary_key?(table_name = nil)
583
+ true
584
+ end
585
+
586
+ def default_sequence_name(table, column) #:nodoc:
587
+ quote_table_name("#{table}_seq")
588
+ end
589
+
590
+
591
+ # Inserts the given fixture into the table. Overridden to properly handle lobs.
592
+ def insert_fixture(fixture, table_name) #:nodoc:
593
+ super
594
+
595
+ klass = fixture.class_name.constantize rescue nil
596
+ if klass.respond_to?(:ancestors) && klass.ancestors.include?(ActiveRecord::Base)
597
+ write_lobs(table_name, klass, fixture)
598
+ end
599
+ end
600
+
601
+ # Writes LOB values from attributes, as indicated by the LOB columns of klass.
602
+ def write_lobs(table_name, klass, attributes) #:nodoc:
603
+ # is class with composite primary key>
604
+ is_with_cpk = klass.respond_to?(:composite?) && klass.composite?
605
+ if is_with_cpk
606
+ id = klass.primary_key.map {|pk| attributes[pk.to_s] }
607
+ else
608
+ id = quote(attributes[klass.primary_key])
609
+ end
610
+ klass.columns.select { |col| col.sql_type =~ /LOB$/i }.each do |col|
611
+ value = attributes[col.name]
612
+ # changed sequence of next two lines - should check if value is nil before converting to yaml
613
+ next if value.nil? || (value == '')
614
+ value = value.to_yaml if col.text? && klass.serialized_attributes[col.name]
615
+ uncached do
616
+ if is_with_cpk
617
+ lob = select_one("SELECT #{col.name} FROM #{table_name} WHERE #{klass.composite_where_clause(id)} FOR UPDATE",
618
+ 'Writable Large Object')[col.name]
619
+ else
620
+ lob = select_one("SELECT #{col.name} FROM #{table_name} WHERE #{klass.primary_key} = #{id} FOR UPDATE",
621
+ 'Writable Large Object')[col.name]
622
+ end
623
+ @connection.write_lob(lob, value.to_s, col.type == :binary)
624
+ end
625
+ end
626
+ end
627
+
628
+ # change LOB column for ORDER BY clause
629
+ # just first 100 characters are taken for ordering
630
+ def lob_order_by_expression(klass, order) #:nodoc:
631
+ return order if order.nil?
632
+ changed = false
633
+ new_order = order.to_s.strip.split(/, */).map do |order_by_col|
634
+ column_name, asc_desc = order_by_col.split(/ +/)
635
+ if column = klass.columns.detect { |col| col.name == column_name && col.sql_type =~ /LOB$/i}
636
+ changed = true
637
+ "DBMS_LOB.SUBSTR(#{column_name},100,1) #{asc_desc}"
638
+ else
639
+ order_by_col
640
+ end
641
+ end.join(', ')
642
+ changed ? new_order : order
643
+ end
644
+
645
+ # SCHEMA STATEMENTS ========================================
646
+ #
647
+ # see: abstract/schema_statements.rb
648
+
649
+ def current_database #:nodoc:
650
+ select_one("select sys_context('userenv','db_name') db from dual")["db"]
651
+ end
652
+
653
+ def tables(name = nil) #:nodoc:
654
+ # changed select from user_tables to all_tables - much faster in large data dictionaries
655
+ select_all("select decode(table_name,upper(table_name),lower(table_name),table_name) name from all_tables where owner = sys_context('userenv','session_user')").map {|t| t['name']}
656
+ end
657
+
658
+ cattr_accessor :all_schema_indexes #:nodoc:
659
+
660
+ # This method selects all indexes at once, and caches them in a class variable.
661
+ # Subsequent index calls get them from the variable, without going to the DB.
662
+ def indexes(table_name, name = nil) #:nodoc:
663
+ (owner, table_name) = @connection.describe(table_name)
664
+ unless all_schema_indexes
665
+ result = select_all(<<-SQL)
666
+ SELECT lower(i.table_name) as table_name, lower(i.index_name) as index_name, i.uniqueness, lower(c.column_name) as column_name
667
+ FROM all_indexes i, all_ind_columns c
668
+ WHERE i.owner = '#{owner}'
669
+ AND i.table_owner = '#{owner}'
670
+ AND c.index_name = i.index_name
671
+ AND c.index_owner = i.owner
672
+ 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')
673
+ ORDER BY i.index_name, c.column_position
674
+ SQL
675
+
676
+ current_index = nil
677
+ self.all_schema_indexes = []
678
+
679
+ result.each do |row|
680
+ # have to keep track of indexes because above query returns dups
681
+ # there is probably a better query we could figure out
682
+ if current_index != row['index_name']
683
+ self.all_schema_indexes << ::ActiveRecord::ConnectionAdapters::IndexDefinition.new(row['table_name'], row['index_name'], row['uniqueness'] == "UNIQUE", [])
684
+ current_index = row['index_name']
685
+ end
686
+
687
+ self.all_schema_indexes.last.columns << row['column_name']
688
+ end
689
+ end
690
+
691
+ # Return the indexes just for the requested table, since AR is structured that way
692
+ table_name = table_name.downcase
693
+ all_schema_indexes.select{|i| i.table == table_name}
694
+ end
695
+
696
+ # set ignored columns for table
697
+ def ignore_table_columns(table_name, *args) #:nodoc:
698
+ @ignore_table_columns ||= {}
699
+ @ignore_table_columns[table_name] ||= []
700
+ @ignore_table_columns[table_name] += args.map{|a| a.to_s.downcase}
701
+ @ignore_table_columns[table_name].uniq!
702
+ end
703
+
704
+ def ignored_table_columns(table_name) #:nodoc:
705
+ @ignore_table_columns ||= {}
706
+ @ignore_table_columns[table_name]
707
+ end
708
+
709
+ # set explicit type for specified table columns
710
+ def set_type_for_columns(table_name, column_type, *args) #:nodoc:
711
+ @table_column_type ||= {}
712
+ @table_column_type[table_name] ||= {}
713
+ args.each do |col|
714
+ @table_column_type[table_name][col.to_s.downcase] = column_type
715
+ end
716
+ end
717
+
718
+ def get_type_for_column(table_name, column_name) #:nodoc:
719
+ @table_column_type && @table_column_type[table_name] && @table_column_type[table_name][column_name.to_s.downcase]
720
+ end
721
+
722
+ def clear_types_for_columns #:nodoc:
723
+ @table_column_type = nil
724
+ end
725
+
726
+ def columns(table_name, name = nil) #:nodoc:
727
+ # get ignored_columns by original table name
728
+ ignored_columns = ignored_table_columns(table_name)
729
+
730
+ (owner, desc_table_name) = @connection.describe(table_name)
731
+
732
+ table_cols = <<-SQL
733
+ select column_name as name, data_type as sql_type, data_default, nullable,
734
+ decode(data_type, 'NUMBER', data_precision,
735
+ 'FLOAT', data_precision,
736
+ 'VARCHAR2', decode(char_used, 'C', char_length, data_length),
737
+ 'CHAR', decode(char_used, 'C', char_length, data_length),
738
+ null) as limit,
739
+ decode(data_type, 'NUMBER', data_scale, null) as scale
740
+ from all_tab_columns
741
+ where owner = '#{owner}'
742
+ and table_name = '#{desc_table_name}'
743
+ order by column_id
744
+ SQL
745
+
746
+ # added deletion of ignored columns
747
+ select_all(table_cols, name).delete_if do |row|
748
+ ignored_columns && ignored_columns.include?(row['name'].downcase)
749
+ end.map do |row|
750
+ limit, scale = row['limit'], row['scale']
751
+ if limit || scale
752
+ row['sql_type'] << "(#{(limit || 38).to_i}" + ((scale = scale.to_i) > 0 ? ",#{scale})" : ")")
753
+ end
754
+
755
+ # clean up odd default spacing from Oracle
756
+ if row['data_default']
757
+ row['data_default'].sub!(/^(.*?)\s*$/, '\1')
758
+ row['data_default'].sub!(/^'(.*)'$/, '\1')
759
+ row['data_default'] = nil if row['data_default'] =~ /^(null|empty_[bc]lob\(\))$/i
760
+ end
761
+
762
+ OracleEnhancedColumn.new(oracle_downcase(row['name']),
763
+ row['data_default'],
764
+ row['sql_type'],
765
+ row['nullable'] == 'Y',
766
+ # pass table name for table specific column definitions
767
+ table_name,
768
+ # pass column type if specified in class definition
769
+ get_type_for_column(table_name, oracle_downcase(row['name'])))
770
+ end
771
+ end
772
+
773
+ ##
774
+ # :singleton-method:
775
+ # Specify default sequence start with value (by default 10000 if not explicitly set), e.g.:
776
+ #
777
+ # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.default_sequence_start_value = 1
778
+ cattr_accessor :default_sequence_start_value
779
+ self.default_sequence_start_value = 10000
780
+
781
+ # Additional options for +create_table+ method in migration files.
782
+ #
783
+ # You can specify individual starting value in table creation migration file, e.g.:
784
+ #
785
+ # create_table :users, :sequence_start_value => 100 do |t|
786
+ # # ...
787
+ # end
788
+ #
789
+ # You can also specify other sequence definition additional parameters, e.g.:
790
+ #
791
+ # create_table :users, :sequence_start_value => “100 NOCACHE INCREMENT BY 10” do |t|
792
+ # # ...
793
+ # end
794
+ #
795
+ # It is possible to add table and column comments in table creation migration files:
796
+ #
797
+ # create_table :employees, :comment => “Employees and contractors” do |t|
798
+ # t.string :first_name, :comment => “Given name”
799
+ # t.string :last_name, :comment => “Surname”
800
+ # end
801
+ def create_table(name, options = {}, &block)
802
+ create_sequence = options[:id] != false
803
+ column_comments = {}
804
+ super(name, options) do |t|
805
+ # store that primary key was defined in create_table block
806
+ unless create_sequence
807
+ class <<t
808
+ attr_accessor :create_sequence
809
+ def primary_key(*args)
810
+ self.create_sequence = true
811
+ super(*args)
812
+ end
813
+ end
814
+ end
815
+
816
+ # store column comments
817
+ class <<t
818
+ attr_accessor :column_comments
819
+ def column(name, type, options = {})
820
+ if options[:comment]
821
+ self.column_comments ||= {}
822
+ self.column_comments[name] = options[:comment]
823
+ end
824
+ super(name, type, options)
825
+ end
826
+ end
827
+
828
+ result = block.call(t) if block
829
+ create_sequence = create_sequence || t.create_sequence
830
+ column_comments = t.column_comments if t.column_comments
831
+ end
832
+
833
+ seq_name = options[:sequence_name] || quote_table_name("#{name}_seq")
834
+ seq_start_value = options[:sequence_start_value] || default_sequence_start_value
835
+ execute "CREATE SEQUENCE #{seq_name} START WITH #{seq_start_value}" if create_sequence
836
+
837
+ add_table_comment name, options[:comment]
838
+ column_comments.each do |column_name, comment|
839
+ add_comment name, column_name, comment
840
+ end
841
+
842
+ end
843
+
844
+ def rename_table(name, new_name) #:nodoc:
845
+ execute "RENAME #{quote_table_name(name)} TO #{quote_table_name(new_name)}"
846
+ execute "RENAME #{quote_table_name("#{name}_seq")} TO #{quote_table_name("#{new_name}_seq")}" rescue nil
847
+ end
848
+
849
+ def drop_table(name, options = {}) #:nodoc:
850
+ super(name)
851
+ seq_name = options[:sequence_name] || quote_table_name("#{name}_seq")
852
+ execute "DROP SEQUENCE #{seq_name}" rescue nil
853
+ end
854
+
855
+ # clear cached indexes when adding new index
856
+ def add_index(table_name, column_name, options = {}) #:nodoc:
857
+ self.all_schema_indexes = nil
858
+ super
859
+ end
860
+
861
+ # clear cached indexes when removing index
862
+ def remove_index(table_name, options = {}) #:nodoc:
863
+ self.all_schema_indexes = nil
864
+ execute "DROP INDEX #{index_name(table_name, options)}"
865
+ end
866
+
867
+ # returned shortened index name if default is too large
868
+ def index_name(table_name, options) #:nodoc:
869
+ default_name = super(table_name, options)
870
+ return default_name if default_name.length <= IDENTIFIER_MAX_LENGTH
871
+
872
+ # remove 'index', 'on' and 'and' keywords
873
+ shortened_name = "i_#{table_name}_#{Array(options[:column]) * '_'}"
874
+
875
+ # leave just first three letters from each word
876
+ if shortened_name.length > IDENTIFIER_MAX_LENGTH
877
+ shortened_name = shortened_name.split('_').map{|w| w[0,3]}.join('_')
878
+ end
879
+
880
+ if shortened_name.length <= IDENTIFIER_MAX_LENGTH
881
+ @logger.warn "#{adapter_name} shortened index name #{default_name} to #{shortened_name}" if @logger
882
+ return shortened_name
883
+ else
884
+ raise ArgumentError, "#{adapter_name} cannot shorten index name #{default_name}, please use add_index with :name option"
885
+ end
886
+ end
887
+
888
+ def add_column(table_name, column_name, type, options = {}) #:nodoc:
889
+ 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])}"
890
+ options[:type] = type
891
+ add_column_options!(add_column_sql, options)
892
+ execute(add_column_sql)
893
+ end
894
+
895
+ def change_column_default(table_name, column_name, default) #:nodoc:
896
+ execute "ALTER TABLE #{quote_table_name(table_name)} MODIFY #{quote_column_name(column_name)} DEFAULT #{quote(default)}"
897
+ end
898
+
899
+ def change_column_null(table_name, column_name, null, default = nil) #:nodoc:
900
+ column = column_for(table_name, column_name)
901
+
902
+ unless null || default.nil?
903
+ execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
904
+ end
905
+
906
+ change_column table_name, column_name, column.sql_type, :null => null
907
+ end
908
+
909
+ def change_column(table_name, column_name, type, options = {}) #:nodoc:
910
+ column = column_for(table_name, column_name)
911
+
912
+ # remove :null option if its value is the same as current column definition
913
+ # otherwise Oracle will raise error
914
+ if options.has_key?(:null) && options[:null] == column.null
915
+ options[:null] = nil
916
+ end
917
+
918
+ 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])}"
919
+ options[:type] = type
920
+ add_column_options!(change_column_sql, options)
921
+ execute(change_column_sql)
922
+ end
923
+
924
+ def rename_column(table_name, column_name, new_column_name) #:nodoc:
925
+ execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} to #{quote_column_name(new_column_name)}"
926
+ end
927
+
928
+ def remove_column(table_name, column_name) #:nodoc:
929
+ execute "ALTER TABLE #{quote_table_name(table_name)} DROP COLUMN #{quote_column_name(column_name)}"
930
+ end
931
+
932
+ def add_comment(table_name, column_name, comment) #:nodoc:
933
+ return if comment.blank?
934
+ execute "COMMENT ON COLUMN #{quote_table_name(table_name)}.#{column_name} IS '#{comment}'"
935
+ end
936
+
937
+ def add_table_comment(table_name, comment) #:nodoc:
938
+ return if comment.blank?
939
+ execute "COMMENT ON TABLE #{quote_table_name(table_name)} IS '#{comment}'"
940
+ end
941
+
942
+ def table_comment(table_name) #:nodoc:
943
+ (owner, table_name) = @connection.describe(table_name)
944
+ select_value <<-SQL
945
+ SELECT comments FROM all_tab_comments
946
+ WHERE owner = '#{owner}'
947
+ AND table_name = '#{table_name}'
948
+ SQL
949
+ end
950
+
951
+ def column_comment(table_name, column_name) #:nodoc:
952
+ (owner, table_name) = @connection.describe(table_name)
953
+ select_value <<-SQL
954
+ SELECT comments FROM all_col_comments
955
+ WHERE owner = '#{owner}'
956
+ AND table_name = '#{table_name}'
957
+ AND column_name = '#{column_name.upcase}'
958
+ SQL
959
+ end
960
+
961
+ # Find a table's primary key and sequence.
962
+ # *Note*: Only primary key is implemented - sequence will be nil.
963
+ def pk_and_sequence_for(table_name) #:nodoc:
964
+ (owner, table_name) = @connection.describe(table_name)
965
+
966
+ # changed select from all_constraints to user_constraints - much faster in large data dictionaries
967
+ pks = select_values(<<-SQL, 'Primary Key')
968
+ select cc.column_name
969
+ from user_constraints c, user_cons_columns cc
970
+ where c.owner = '#{owner}'
971
+ and c.table_name = '#{table_name}'
972
+ and c.constraint_type = 'P'
973
+ and cc.owner = c.owner
974
+ and cc.constraint_name = c.constraint_name
975
+ SQL
976
+
977
+ # only support single column keys
978
+ pks.size == 1 ? [oracle_downcase(pks.first), nil] : nil
979
+ end
980
+
981
+ def structure_dump #:nodoc:
982
+ s = select_all("select sequence_name from user_sequences order by 1").inject("") do |structure, seq|
983
+ structure << "create sequence #{seq.to_a.first.last};\n\n"
984
+ end
985
+
986
+ # changed select from user_tables to all_tables - much faster in large data dictionaries
987
+ select_all("select table_name from all_tables where owner = sys_context('userenv','session_user') order by 1").inject(s) do |structure, table|
988
+ ddl = "create table #{table.to_a.first.last} (\n "
989
+ cols = select_all(%Q{
990
+ select column_name, data_type, data_length, char_used, char_length, data_precision, data_scale, data_default, nullable
991
+ from user_tab_columns
992
+ where table_name = '#{table.to_a.first.last}'
993
+ order by column_id
994
+ }).map do |row|
995
+ col = "#{row['column_name'].downcase} #{row['data_type'].downcase}"
996
+ if row['data_type'] =='NUMBER' and !row['data_precision'].nil?
997
+ col << "(#{row['data_precision'].to_i}"
998
+ col << ",#{row['data_scale'].to_i}" if !row['data_scale'].nil?
999
+ col << ')'
1000
+ elsif row['data_type'].include?('CHAR')
1001
+ length = row['char_used'] == 'C' ? row['char_length'].to_i : row['data_length'].to_i
1002
+ col << "(#{length})"
1003
+ end
1004
+ col << " default #{row['data_default']}" if !row['data_default'].nil?
1005
+ col << ' not null' if row['nullable'] == 'N'
1006
+ col
1007
+ end
1008
+ ddl << cols.join(",\n ")
1009
+ ddl << ");\n\n"
1010
+ structure << ddl
1011
+ end
1012
+ end
1013
+
1014
+ # Extract all stored procedures, packages, synonyms and views.
1015
+ def structure_dump_db_stored_code #:nodoc:
1016
+ structure = "\n"
1017
+ select_all("select distinct name, type
1018
+ from all_source
1019
+ where type in ('PROCEDURE', 'PACKAGE', 'PACKAGE BODY', 'FUNCTION')
1020
+ and owner = sys_context('userenv','session_user')").inject("\n\n") do |structure, source|
1021
+ ddl = "create or replace \n "
1022
+ lines = select_all(%Q{
1023
+ select text
1024
+ from all_source
1025
+ where name = '#{source['name']}'
1026
+ and type = '#{source['type']}'
1027
+ and owner = sys_context('userenv','session_user')
1028
+ order by line
1029
+ }).map do |row|
1030
+ ddl << row['text'] if row['text'].size > 1
1031
+ end
1032
+ ddl << ";"
1033
+ structure << ddl << "\n"
1034
+ end
1035
+
1036
+ # export views
1037
+ select_all("select view_name, text from user_views").inject(structure) do |structure, view|
1038
+ ddl = "create or replace view #{view['view_name']} AS\n "
1039
+ # any views with empty lines will cause OCI to barf when loading. remove blank lines =/
1040
+ ddl << view['text'].gsub(/^\n/, '')
1041
+ ddl << ";\n\n"
1042
+ structure << ddl
1043
+ end
1044
+
1045
+ # export synonyms
1046
+ select_all("select owner, synonym_name, table_name, table_owner
1047
+ from all_synonyms
1048
+ where table_owner = sys_context('userenv','session_user') ").inject(structure) do |structure, synonym|
1049
+ ddl = "create or replace #{synonym['owner'] == 'PUBLIC' ? 'PUBLIC' : '' } SYNONYM #{synonym['synonym_name']} for #{synonym['table_owner']}.#{synonym['table_name']};\n\n"
1050
+ structure << ddl;
1051
+ end
1052
+ end
1053
+
1054
+
1055
+ def structure_drop #:nodoc:
1056
+ s = select_all("select sequence_name from user_sequences order by 1").inject("") do |drop, seq|
1057
+ drop << "drop sequence #{seq.to_a.first.last};\n\n"
1058
+ end
1059
+
1060
+ # changed select from user_tables to all_tables - much faster in large data dictionaries
1061
+ select_all("select table_name from all_tables where owner = sys_context('userenv','session_user') order by 1").inject(s) do |drop, table|
1062
+ drop << "drop table #{table.to_a.first.last} cascade constraints;\n\n"
1063
+ end
1064
+ end
1065
+
1066
+
1067
+ def add_column_options!(sql, options) #:nodoc:
1068
+ type = options[:type] || ((column = options[:column]) && column.type)
1069
+ type = type && type.to_sym
1070
+ # handle case of defaults for CLOB columns, which would otherwise get "quoted" incorrectly
1071
+ if options_include_default?(options)
1072
+ if type == :text
1073
+ sql << " DEFAULT #{quote(options[:default])}"
1074
+ else
1075
+ # from abstract adapter
1076
+ sql << " DEFAULT #{quote(options[:default], options[:column])}"
1077
+ end
1078
+ end
1079
+ # must explicitly add NULL or NOT NULL to allow change_column to work on migrations
1080
+ if options[:null] == false
1081
+ sql << " NOT NULL"
1082
+ elsif options[:null] == true
1083
+ sql << " NULL" unless type == :primary_key
1084
+ end
1085
+ end
1086
+
1087
+ # SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
1088
+ #
1089
+ # Oracle requires the ORDER BY columns to be in the SELECT list for DISTINCT
1090
+ # queries. However, with those columns included in the SELECT DISTINCT list, you
1091
+ # won't actually get a distinct list of the column you want (presuming the column
1092
+ # has duplicates with multiple values for the ordered-by columns. So we use the
1093
+ # FIRST_VALUE function to get a single (first) value for each column, effectively
1094
+ # making every row the same.
1095
+ #
1096
+ # distinct("posts.id", "posts.created_at desc")
1097
+ def distinct(columns, order_by) #:nodoc:
1098
+ return "DISTINCT #{columns}" if order_by.blank?
1099
+
1100
+ # construct a valid DISTINCT clause, ie. one that includes the ORDER BY columns, using
1101
+ # FIRST_VALUE such that the inclusion of these columns doesn't invalidate the DISTINCT
1102
+ order_columns = order_by.split(',').map { |s| s.strip }.reject(&:blank?)
1103
+ order_columns = order_columns.zip((0...order_columns.size).to_a).map do |c, i|
1104
+ "FIRST_VALUE(#{c.split.first}) OVER (PARTITION BY #{columns} ORDER BY #{c}) AS alias_#{i}__"
1105
+ end
1106
+ sql = "DISTINCT #{columns}, "
1107
+ sql << order_columns * ", "
1108
+ end
1109
+
1110
+ # ORDER BY clause for the passed order option.
1111
+ #
1112
+ # Uses column aliases as defined by #distinct.
1113
+ def add_order_by_for_association_limiting!(sql, options) #:nodoc:
1114
+ return sql if options[:order].blank?
1115
+
1116
+ order = options[:order].split(',').collect { |s| s.strip }.reject(&:blank?)
1117
+ order.map! {|s| $1 if s =~ / (.*)/}
1118
+ order = order.zip((0...order.size).to_a).map { |s,i| "alias_#{i}__ #{s}" }.join(', ')
1119
+
1120
+ sql << " ORDER BY #{order}"
1121
+ end
1122
+
1123
+ protected
1124
+
1125
+ def translate_exception(exception, message) #:nodoc:
1126
+ case @connection.error_code(exception)
1127
+ when 1
1128
+ RecordNotUnique.new(message, exception)
1129
+ when 2291
1130
+ InvalidForeignKey.new(message, exception)
1131
+ else
1132
+ super
1133
+ end
1134
+ end
1135
+
1136
+ private
1137
+
1138
+ def select(sql, name = nil, return_column_names = false)
1139
+ log(sql, name) do
1140
+ @connection.select(sql, name, return_column_names)
1141
+ end
1142
+ end
1143
+
1144
+ def oracle_downcase(column_name)
1145
+ @connection.oracle_downcase(column_name)
1146
+ end
1147
+
1148
+ def column_for(table_name, column_name)
1149
+ unless column = columns(table_name).find { |c| c.name == column_name.to_s }
1150
+ raise "No such column: #{table_name}.#{column_name}"
1151
+ end
1152
+ column
1153
+ end
1154
+
1155
+ end
1156
+ end
1157
+ end
1158
+
1159
+ # Added LOB writing callback for sessions stored in database
1160
+ # Otherwise it is not working as Session class is defined before OracleAdapter is loaded in Rails 2.0
1161
+ if defined?(CGI::Session::ActiveRecordStore::Session)
1162
+ if !CGI::Session::ActiveRecordStore::Session.respond_to?(:after_save_callback_chain) ||
1163
+ CGI::Session::ActiveRecordStore::Session.after_save_callback_chain.detect{|cb| cb.method == :enhanced_write_lobs}.nil?
1164
+ #:stopdoc:
1165
+ class CGI::Session::ActiveRecordStore::Session
1166
+ after_save :enhanced_write_lobs
1167
+ end
1168
+ #:startdoc:
1169
+ end
1170
+ end
1171
+
1172
+ # Load custom create, update, delete methods functionality
1173
+ # rescue LoadError if ruby-plsql gem cannot be loaded
1174
+ begin
1175
+ require 'active_record/connection_adapters/oracle_enhanced_procedures'
1176
+ rescue LoadError
1177
+ if defined?(RAILS_DEFAULT_LOGGER)
1178
+ RAILS_DEFAULT_LOGGER.info "INFO: ActiveRecord oracle_enhanced adapter could not load ruby-plsql gem. "+
1179
+ "Custom create, update and delete methods will not be available."
1180
+ end
1181
+ end
1182
+
1183
+ # Load additional methods for composite_primary_keys support
1184
+ require 'active_record/connection_adapters/oracle_enhanced_cpk'
1185
+
1186
+ # Load patch for dirty tracking methods
1187
+ require 'active_record/connection_adapters/oracle_enhanced_dirty'
1188
+
1189
+ # Load rake tasks definitions
1190
+ begin
1191
+ require 'active_record/connection_adapters/oracle_enhanced_tasks'
1192
+ rescue LoadError
1193
+ end if defined?(RAILS_ROOT)
1194
+
1195
+ # Handles quoting of oracle reserved words
1196
+ require 'active_record/connection_adapters/oracle_enhanced_reserved_words'
1197
+
1198
+ # Add BigDecimal#to_d, Fixnum#to_d and Bignum#to_d methods if not already present
1199
+ require 'active_record/connection_adapters/oracle_enhanced_core_ext'
1200
+