c3-activerecord-oracle_enhanced-adapter 1.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/History.txt +182 -0
  2. data/License.txt +20 -0
  3. data/Manifest.txt +32 -0
  4. data/README.rdoc +77 -0
  5. data/Rakefile +49 -0
  6. data/VERSION +1 -0
  7. data/activerecord-oracle_enhanced-adapter.gemspec +79 -0
  8. data/lib/active_record/connection_adapters/emulation/oracle_adapter.rb +5 -0
  9. data/lib/active_record/connection_adapters/oracle_enhanced.rake +51 -0
  10. data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +1664 -0
  11. data/lib/active_record/connection_adapters/oracle_enhanced_connection.rb +121 -0
  12. data/lib/active_record/connection_adapters/oracle_enhanced_core_ext.rb +64 -0
  13. data/lib/active_record/connection_adapters/oracle_enhanced_cpk.rb +21 -0
  14. data/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb +39 -0
  15. data/lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb +393 -0
  16. data/lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb +389 -0
  17. data/lib/active_record/connection_adapters/oracle_enhanced_procedures.rb +163 -0
  18. data/lib/active_record/connection_adapters/oracle_enhanced_reserved_words.rb +126 -0
  19. data/lib/active_record/connection_adapters/oracle_enhanced_schema_definitions.rb +168 -0
  20. data/lib/active_record/connection_adapters/oracle_enhanced_schema_dumper.rb +213 -0
  21. data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements_ext.rb +224 -0
  22. data/lib/active_record/connection_adapters/oracle_enhanced_tasks.rb +11 -0
  23. data/lib/active_record/connection_adapters/oracle_enhanced_version.rb +1 -0
  24. data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +477 -0
  25. data/spec/active_record/connection_adapters/oracle_enhanced_adapter_structure_dumper_spec.rb +267 -0
  26. data/spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb +206 -0
  27. data/spec/active_record/connection_adapters/oracle_enhanced_core_ext_spec.rb +40 -0
  28. data/spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb +107 -0
  29. data/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +984 -0
  30. data/spec/active_record/connection_adapters/oracle_enhanced_dbms_output_spec.rb +67 -0
  31. data/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb +93 -0
  32. data/spec/active_record/connection_adapters/oracle_enhanced_emulate_oracle_adapter_spec.rb +25 -0
  33. data/spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb +370 -0
  34. data/spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb +218 -0
  35. data/spec/active_record/connection_adapters/oracle_enhanced_schema_spec.rb +784 -0
  36. data/spec/spec.opts +6 -0
  37. data/spec/spec_helper.rb +114 -0
  38. metadata +137 -0
@@ -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,51 @@
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
+ File.open("db/#{RAILS_ENV}_structure.sql", "a") { |f| f << ActiveRecord::Base.connection.structure_dump_fk_constraints }
21
+ if ActiveRecord::Base.connection.supports_migrations?
22
+ File.open("db/#{RAILS_ENV}_structure.sql", "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information }
23
+ end
24
+ if abcs[RAILS_ENV]['structure_dump'] == "db_stored_code"
25
+ File.open("db/#{RAILS_ENV}_structure.sql", "a") { |f| f << ActiveRecord::Base.connection.structure_dump_db_stored_code }
26
+ end
27
+
28
+ end
29
+ end
30
+
31
+ namespace :test do
32
+ redefine_task :clone_structure => [ "db:structure:dump", "db:test:purge" ] do
33
+ abcs = ActiveRecord::Base.configurations
34
+ ActiveRecord::Base.establish_connection(:test)
35
+ IO.readlines("db/#{RAILS_ENV}_structure.sql").join.split("\n\n").each do |ddl|
36
+ ddl.chop! if ddl.last == ";"
37
+ ActiveRecord::Base.connection.execute(ddl) unless ddl.blank?
38
+ end
39
+ end
40
+
41
+ redefine_task :purge => :environment do
42
+ abcs = ActiveRecord::Base.configurations
43
+ ActiveRecord::Base.establish_connection(:test)
44
+ ActiveRecord::Base.connection.full_drop.split("\n\n").each do |ddl|
45
+ ddl.chop! if ddl.last == ";"
46
+ ActiveRecord::Base.connection.execute(ddl) unless ddl.blank?
47
+ end
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,1664 @@
1
+ # -*- coding: utf-8 -*-
2
+ # oracle_enhanced_adapter.rb -- ActiveRecord adapter for Oracle 8i, 9i, 10g, 11g
3
+ #
4
+ # Authors or original oracle_adapter: Graham Jenkins, Michael Schoen
5
+ #
6
+ # Current maintainer: Raimonds Simanovskis (http://blog.rayapps.com)
7
+ #
8
+ #########################################################################
9
+ #
10
+ # See History.txt for changes added to original oracle_adapter.rb
11
+ #
12
+ #########################################################################
13
+ #
14
+ # From original oracle_adapter.rb:
15
+ #
16
+ # Implementation notes:
17
+ # 1. Redefines (safely) a method in ActiveRecord to make it possible to
18
+ # implement an autonumbering solution for Oracle.
19
+ # 2. The OCI8 driver is patched to properly handle values for LONG and
20
+ # TIMESTAMP columns. The driver-author has indicated that a future
21
+ # release of the driver will obviate this patch.
22
+ # 3. LOB support is implemented through an after_save callback.
23
+ # 4. Oracle does not offer native LIMIT and OFFSET options; this
24
+ # functionality is mimiced through the use of nested selects.
25
+ # See http://asktom.oracle.com/pls/ask/f?p=4950:8:::::F4950_P8_DISPLAYID:127412348064
26
+ #
27
+ # Do what you want with this code, at your own peril, but if any
28
+ # significant portion of my code remains then please acknowledge my
29
+ # contribution.
30
+ # portions Copyright 2005 Graham Jenkins
31
+
32
+ require 'active_record/connection_adapters/abstract_adapter'
33
+
34
+ require 'active_record/connection_adapters/oracle_enhanced_connection'
35
+
36
+ require 'digest/sha1'
37
+
38
+ module ActiveRecord
39
+ class Base
40
+ # Establishes a connection to the database that's used by all Active Record objects.
41
+ def self.oracle_enhanced_connection(config) #:nodoc:
42
+ if config[:emulate_oracle_adapter] == true
43
+ # allows the enhanced adapter to look like the OracleAdapter. Useful to pick up
44
+ # conditionals in the rails activerecord test suite
45
+ require 'active_record/connection_adapters/emulation/oracle_adapter'
46
+ ConnectionAdapters::OracleAdapter.new(
47
+ ConnectionAdapters::OracleEnhancedConnection.create(config), logger)
48
+ else
49
+ ConnectionAdapters::OracleEnhancedAdapter.new(
50
+ ConnectionAdapters::OracleEnhancedConnection.create(config), logger)
51
+ end
52
+ end
53
+
54
+ # Specify table columns which should be ignored by ActiveRecord, e.g.:
55
+ #
56
+ # ignore_table_columns :attribute1, :attribute2
57
+ def self.ignore_table_columns(*args)
58
+ connection.ignore_table_columns(table_name,*args)
59
+ end
60
+
61
+ # Specify which table columns should be typecasted to Date (without time), e.g.:
62
+ #
63
+ # set_date_columns :created_on, :updated_on
64
+ def self.set_date_columns(*args)
65
+ connection.set_type_for_columns(table_name,:date,*args)
66
+ end
67
+
68
+ # Specify which table columns should be typecasted to Time (or DateTime), e.g.:
69
+ #
70
+ # set_datetime_columns :created_date, :updated_date
71
+ def self.set_datetime_columns(*args)
72
+ connection.set_type_for_columns(table_name,:datetime,*args)
73
+ end
74
+
75
+ # Specify which table columns should be typecasted to boolean values +true+ or +false+, e.g.:
76
+ #
77
+ # set_boolean_columns :is_valid, :is_completed
78
+ def self.set_boolean_columns(*args)
79
+ connection.set_type_for_columns(table_name,:boolean,*args)
80
+ end
81
+
82
+ # Specify which table columns should be typecasted to integer values.
83
+ # Might be useful to force NUMBER(1) column to be integer and not boolean, or force NUMBER column without
84
+ # scale to be retrieved as integer and not decimal. Example:
85
+ #
86
+ # set_integer_columns :version_number, :object_identifier
87
+ def self.set_integer_columns(*args)
88
+ connection.set_type_for_columns(table_name,:integer,*args)
89
+ end
90
+
91
+ # Specify which table columns should be typecasted to string values.
92
+ # Might be useful to specify that columns should be string even if its name matches boolean column criteria.
93
+ #
94
+ # set_integer_columns :active_flag
95
+ def self.set_string_columns(*args)
96
+ connection.set_type_for_columns(table_name,:string,*args)
97
+ end
98
+
99
+ # After setting large objects to empty, select the OCI8::LOB
100
+ # and write back the data.
101
+ after_save :enhanced_write_lobs
102
+ def enhanced_write_lobs #:nodoc:
103
+ if connection.is_a?(ConnectionAdapters::OracleEnhancedAdapter) &&
104
+ !(self.class.custom_create_method || self.class.custom_update_method)
105
+ connection.write_lobs(self.class.table_name, self.class, attributes)
106
+ end
107
+ end
108
+ private :enhanced_write_lobs
109
+
110
+ class << self
111
+ # patch ORDER BY to work with LOBs
112
+ def add_order_with_lobs!(sql, order, scope = :auto)
113
+ if connection.is_a?(ConnectionAdapters::OracleEnhancedAdapter)
114
+ order = connection.lob_order_by_expression(self, order) if order
115
+
116
+ orig_scope = scope
117
+ scope = scope(:find) if :auto == scope
118
+ if scope
119
+ new_scope_order = connection.lob_order_by_expression(self, scope[:order])
120
+ if new_scope_order != scope[:order]
121
+ scope = scope.merge(:order => new_scope_order)
122
+ else
123
+ scope = orig_scope
124
+ end
125
+ end
126
+ end
127
+ add_order_without_lobs!(sql, order, scope = :auto)
128
+ end
129
+ private :add_order_with_lobs!
130
+ #:stopdoc:
131
+ alias_method :add_order_without_lobs!, :add_order!
132
+ alias_method :add_order!, :add_order_with_lobs!
133
+ #:startdoc:
134
+ end
135
+
136
+ # Get table comment from schema definition.
137
+ def self.table_comment
138
+ connection.table_comment(self.table_name)
139
+ end
140
+ end
141
+
142
+
143
+ module ConnectionAdapters #:nodoc:
144
+ class OracleEnhancedColumn < Column
145
+
146
+ attr_reader :table_name, :forced_column_type #:nodoc:
147
+
148
+ def initialize(name, default, sql_type = nil, null = true, table_name = nil, forced_column_type = nil) #:nodoc:
149
+ @table_name = table_name
150
+ @forced_column_type = forced_column_type
151
+ super(name, default, sql_type, null)
152
+ end
153
+
154
+ def type_cast(value) #:nodoc:
155
+ return guess_date_or_time(value) if type == :datetime && OracleEnhancedAdapter.emulate_dates
156
+ super
157
+ end
158
+
159
+ # convert something to a boolean
160
+ # added y as boolean value
161
+ def self.value_to_boolean(value) #:nodoc:
162
+ if value == true || value == false
163
+ value
164
+ elsif value.is_a?(String) && value.blank?
165
+ nil
166
+ else
167
+ %w(true t 1 y +).include?(value.to_s.downcase)
168
+ end
169
+ end
170
+
171
+ # convert Time or DateTime value to Date for :date columns
172
+ def self.string_to_date(string) #:nodoc:
173
+ return string.to_date if string.is_a?(Time) || string.is_a?(DateTime)
174
+ super
175
+ end
176
+
177
+ # convert Date value to Time for :datetime columns
178
+ def self.string_to_time(string) #:nodoc:
179
+ return string.to_time if string.is_a?(Date) && !OracleEnhancedAdapter.emulate_dates
180
+ super
181
+ end
182
+
183
+ # Get column comment from schema definition.
184
+ # Will work only if using default ActiveRecord connection.
185
+ def comment
186
+ ActiveRecord::Base.connection.column_comment(@table_name, name)
187
+ end
188
+
189
+ private
190
+ def simplified_type(field_type)
191
+ forced_column_type ||
192
+ case field_type
193
+ when /decimal|numeric|number/i
194
+ return :boolean if OracleEnhancedAdapter.emulate_booleans && field_type == 'NUMBER(1)'
195
+ return :integer if extract_scale(field_type) == 0
196
+ # if column name is ID or ends with _ID
197
+ return :integer if OracleEnhancedAdapter.emulate_integers_by_column_name && OracleEnhancedAdapter.is_integer_column?(name, table_name)
198
+ :decimal
199
+ when /char/i
200
+ return :boolean if OracleEnhancedAdapter.emulate_booleans_from_strings &&
201
+ OracleEnhancedAdapter.is_boolean_column?(name, field_type, table_name)
202
+ :string
203
+ when /date/i
204
+ forced_column_type ||
205
+ (:date if OracleEnhancedAdapter.emulate_dates_by_column_name && OracleEnhancedAdapter.is_date_column?(name, table_name)) ||
206
+ :datetime
207
+ when /timestamp/i then :timestamp
208
+ when /time/i then :datetime
209
+ else super
210
+ end
211
+ end
212
+
213
+ def guess_date_or_time(value)
214
+ value.respond_to?(:hour) && (value.hour == 0 and value.min == 0 and value.sec == 0) ?
215
+ Date.new(value.year, value.month, value.day) : value
216
+ end
217
+
218
+ class << self
219
+ protected
220
+
221
+ def fallback_string_to_date(string) #:nodoc:
222
+ if OracleEnhancedAdapter.string_to_date_format || OracleEnhancedAdapter.string_to_time_format
223
+ return (string_to_date_or_time_using_format(string).to_date rescue super)
224
+ end
225
+ super
226
+ end
227
+
228
+ def fallback_string_to_time(string) #:nodoc:
229
+ if OracleEnhancedAdapter.string_to_time_format || OracleEnhancedAdapter.string_to_date_format
230
+ return (string_to_date_or_time_using_format(string).to_time rescue super)
231
+ end
232
+ super
233
+ end
234
+
235
+ def string_to_date_or_time_using_format(string) #:nodoc:
236
+ if OracleEnhancedAdapter.string_to_time_format && dt=Date._strptime(string, OracleEnhancedAdapter.string_to_time_format)
237
+ return Time.mktime(*dt.values_at(:year, :mon, :mday, :hour, :min, :sec, :zone, :wday))
238
+ end
239
+ DateTime.strptime(string, OracleEnhancedAdapter.string_to_date_format)
240
+ end
241
+
242
+ end
243
+ end
244
+
245
+
246
+ # Oracle enhanced adapter will work with both
247
+ # Ruby 1.8/1.9 ruby-oci8 gem (which provides interface to Oracle OCI client)
248
+ # or with JRuby and Oracle JDBC driver.
249
+ #
250
+ # It should work with Oracle 9i, 10g and 11g databases.
251
+ # Limited set of functionality should work on Oracle 8i as well but several features
252
+ # rely on newer functionality in Oracle database.
253
+ #
254
+ # Usage notes:
255
+ # * Key generation assumes a "${table_name}_seq" sequence is available
256
+ # for all tables; the sequence name can be changed using
257
+ # ActiveRecord::Base.set_sequence_name. When using Migrations, these
258
+ # sequences are created automatically.
259
+ # Use set_sequence_name :autogenerated with legacy tables that have
260
+ # triggers that populate primary keys automatically.
261
+ # * Oracle uses DATE or TIMESTAMP datatypes for both dates and times.
262
+ # Consequently some hacks are employed to map data back to Date or Time
263
+ # in Ruby. Timezones and sub-second precision on timestamps are
264
+ # not supported.
265
+ # * Default values that are functions (such as "SYSDATE") are not
266
+ # supported. This is a restriction of the way ActiveRecord supports
267
+ # default values.
268
+ #
269
+ # Required parameters:
270
+ #
271
+ # * <tt>:username</tt>
272
+ # * <tt>:password</tt>
273
+ # * <tt>:database</tt> - either TNS alias or connection string for OCI client or database name in JDBC connection string
274
+ #
275
+ # Optional parameters:
276
+ #
277
+ # * <tt>:host</tt> - host name for JDBC connection, defaults to "localhost"
278
+ # * <tt>:port</tt> - port number for JDBC connection, defaults to 1521
279
+ # * <tt>:privilege</tt> - set "SYSDBA" if you want to connect with this privilege
280
+ # * <tt>:allow_concurrency</tt> - set to "true" if non-blocking mode should be enabled (just for OCI client)
281
+ # * <tt>:prefetch_rows</tt> - how many rows should be fetched at one time to increase performance, defaults to 100
282
+ # * <tt>:cursor_sharing</tt> - cursor sharing mode to minimize amount of unique statements, defaults to "force"
283
+ # * <tt>:nls_length_semantics</tt> - semantics of size of VARCHAR2 and CHAR columns, defaults to "CHAR"
284
+ # (meaning that size specifies number of characters and not bytes)
285
+ # * <tt>:time_zone</tt> - database session time zone
286
+ # (it is recommended to set it using ENV['TZ'] which will be then also used for database session time zone)
287
+ class OracleEnhancedAdapter < AbstractAdapter
288
+
289
+ ##
290
+ # :singleton-method:
291
+ # By default, the OracleEnhancedAdapter will consider all columns of type <tt>NUMBER(1)</tt>
292
+ # as boolean. If you wish to disable this emulation you can add the following line
293
+ # to your initializer file:
294
+ #
295
+ # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_booleans = false
296
+ cattr_accessor :emulate_booleans
297
+ self.emulate_booleans = true
298
+
299
+ ##
300
+ # :singleton-method:
301
+ # By default, the OracleEnhancedAdapter will typecast all columns of type <tt>DATE</tt>
302
+ # to Time or DateTime (if value is out of Time value range) value.
303
+ # If you wish that DATE values with hour, minutes and seconds equal to 0 are typecasted
304
+ # to Date then you can add the following line to your initializer file:
305
+ #
306
+ # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_dates = true
307
+ #
308
+ # As this option can have side effects when unnecessary typecasting is done it is recommended
309
+ # that Date columns are explicily defined with +set_date_columns+ method.
310
+ cattr_accessor :emulate_dates
311
+ self.emulate_dates = false
312
+
313
+ ##
314
+ # :singleton-method:
315
+ # By default, the OracleEnhancedAdapter will typecast all columns of type <tt>DATE</tt>
316
+ # to Time or DateTime (if value is out of Time value range) value.
317
+ # If you wish that DATE columns with "date" in their name (e.g. "creation_date") are typecasted
318
+ # to Date then you can add the following line to your initializer file:
319
+ #
320
+ # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_dates_by_column_name = true
321
+ #
322
+ # As this option can have side effects when unnecessary typecasting is done it is recommended
323
+ # that Date columns are explicily defined with +set_date_columns+ method.
324
+ cattr_accessor :emulate_dates_by_column_name
325
+ self.emulate_dates_by_column_name = false
326
+
327
+ # Check column name to identify if it is Date (and not Time) column.
328
+ # Is used if +emulate_dates_by_column_name+ option is set to +true+.
329
+ # Override this method definition in initializer file if different Date column recognition is needed.
330
+ def self.is_date_column?(name, table_name = nil)
331
+ name =~ /(^|_)date(_|$)/i
332
+ end
333
+
334
+ # instance method uses at first check if column type defined at class level
335
+ def is_date_column?(name, table_name = nil) #:nodoc:
336
+ case get_type_for_column(table_name, name)
337
+ when nil
338
+ self.class.is_date_column?(name, table_name)
339
+ when :date
340
+ true
341
+ else
342
+ false
343
+ end
344
+ end
345
+
346
+ ##
347
+ # :singleton-method:
348
+ # By default, the OracleEnhancedAdapter will typecast all columns of type <tt>NUMBER</tt>
349
+ # (without precision or scale) to Float or BigDecimal value.
350
+ # If you wish that NUMBER columns with name "id" or that end with "_id" are typecasted
351
+ # to Integer then you can add the following line to your initializer file:
352
+ #
353
+ # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_integers_by_column_name = true
354
+ cattr_accessor :emulate_integers_by_column_name
355
+ self.emulate_integers_by_column_name = false
356
+
357
+ # Check column name to identify if it is Integer (and not Float or BigDecimal) column.
358
+ # Is used if +emulate_integers_by_column_name+ option is set to +true+.
359
+ # Override this method definition in initializer file if different Integer column recognition is needed.
360
+ def self.is_integer_column?(name, table_name = nil)
361
+ name =~ /(^|_)id$/i
362
+ end
363
+
364
+ ##
365
+ # :singleton-method:
366
+ # If you wish that CHAR(1), VARCHAR2(1) columns or VARCHAR2 columns with FLAG or YN at the end of their name
367
+ # are typecasted to booleans then you can add the following line to your initializer file:
368
+ #
369
+ # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_booleans_from_strings = true
370
+ cattr_accessor :emulate_booleans_from_strings
371
+ self.emulate_booleans_from_strings = false
372
+
373
+ # Check column name to identify if it is boolean (and not String) column.
374
+ # Is used if +emulate_booleans_from_strings+ option is set to +true+.
375
+ # Override this method definition in initializer file if different boolean column recognition is needed.
376
+ def self.is_boolean_column?(name, field_type, table_name = nil)
377
+ return true if ["CHAR(1)","VARCHAR2(1)"].include?(field_type)
378
+ field_type =~ /^VARCHAR2/ && (name =~ /_flag$/i || name =~ /_yn$/i)
379
+ end
380
+
381
+ # How boolean value should be quoted to String.
382
+ # Used if +emulate_booleans_from_strings+ option is set to +true+.
383
+ def self.boolean_to_string(bool)
384
+ bool ? "Y" : "N"
385
+ end
386
+
387
+ ##
388
+ # :singleton-method:
389
+ # Specify non-default date format that should be used when assigning string values to :date columns, e.g.:
390
+ #
391
+ # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.string_to_date_format = “%d.%m.%Y”
392
+ cattr_accessor :string_to_date_format
393
+ self.string_to_date_format = nil
394
+
395
+ ##
396
+ # :singleton-method:
397
+ # Specify non-default time format that should be used when assigning string values to :datetime columns, e.g.:
398
+ #
399
+ # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.string_to_time_format = “%d.%m.%Y %H:%M:%S”
400
+ cattr_accessor :string_to_time_format
401
+ self.string_to_time_format = nil
402
+
403
+ def initialize(connection, logger = nil) #:nodoc:
404
+ super
405
+ @quoted_column_names, @quoted_table_names = {}, {}
406
+ end
407
+
408
+ ADAPTER_NAME = 'OracleEnhanced'.freeze
409
+
410
+ def adapter_name #:nodoc:
411
+ ADAPTER_NAME
412
+ end
413
+
414
+ def supports_migrations? #:nodoc:
415
+ true
416
+ end
417
+
418
+ def supports_savepoints? #:nodoc:
419
+ true
420
+ end
421
+
422
+ #:stopdoc:
423
+ NATIVE_DATABASE_TYPES = {
424
+ :primary_key => "NUMBER(38) NOT NULL PRIMARY KEY",
425
+ :string => { :name => "VARCHAR2", :limit => 255 },
426
+ :text => { :name => "CLOB" },
427
+ :integer => { :name => "NUMBER", :limit => 38 },
428
+ :float => { :name => "NUMBER" },
429
+ :decimal => { :name => "DECIMAL" },
430
+ :datetime => { :name => "DATE" },
431
+ # changed to native TIMESTAMP type
432
+ # :timestamp => { :name => "DATE" },
433
+ :timestamp => { :name => "TIMESTAMP" },
434
+ :time => { :name => "DATE" },
435
+ :date => { :name => "DATE" },
436
+ :binary => { :name => "BLOB" },
437
+ :boolean => { :name => "NUMBER", :limit => 1 }
438
+ }
439
+ # if emulate_booleans_from_strings then store booleans in VARCHAR2
440
+ NATIVE_DATABASE_TYPES_BOOLEAN_STRINGS = NATIVE_DATABASE_TYPES.dup.merge(
441
+ :boolean => { :name => "VARCHAR2", :limit => 1 }
442
+ )
443
+ #:startdoc:
444
+
445
+ def native_database_types #:nodoc:
446
+ emulate_booleans_from_strings ? NATIVE_DATABASE_TYPES_BOOLEAN_STRINGS : NATIVE_DATABASE_TYPES
447
+ end
448
+
449
+ # maximum length of Oracle identifiers
450
+ IDENTIFIER_MAX_LENGTH = 30
451
+
452
+ def table_alias_length #:nodoc:
453
+ IDENTIFIER_MAX_LENGTH
454
+ end
455
+
456
+ # QUOTING ==================================================
457
+ #
458
+ # see: abstract/quoting.rb
459
+
460
+ def quote_column_name(name) #:nodoc:
461
+ # camelCase column names need to be quoted; not that anyone using Oracle
462
+ # would really do this, but handling this case means we pass the test...
463
+ @quoted_column_names[name] = name.to_s =~ /[A-Z]/ ? "\"#{name}\"" : quote_oracle_reserved_words(name)
464
+ end
465
+
466
+ # unescaped table name should start with letter and
467
+ # contain letters, digits, _, $ or #
468
+ # can be prefixed with schema name
469
+ # CamelCase table names should be quoted
470
+ def self.valid_table_name?(name) #:nodoc:
471
+ name = name.to_s
472
+ name =~ /\A([A-Za-z_0-9]+\.)?[a-z][a-z_0-9\$#]*(@[A-Za-z_0-9\.]+)?\Z/ ||
473
+ name =~ /\A([A-Za-z_0-9]+\.)?[A-Z][A-Z_0-9\$#]*(@[A-Za-z_0-9\.]+)?\Z/ ? true : false
474
+ end
475
+
476
+ def quote_table_name(name) #:nodoc:
477
+ # abstract_adapter calls quote_column_name from quote_table_name, so prevent that
478
+ @quoted_table_names[name] ||= if self.class.valid_table_name?(name)
479
+ name
480
+ else
481
+ "\"#{name}\""
482
+ end
483
+ end
484
+
485
+ def quote_string(s) #:nodoc:
486
+ s.gsub(/'/, "''")
487
+ end
488
+
489
+ def quote(value, column = nil) #:nodoc:
490
+ if value && column
491
+ case column.type
492
+ when :text, :binary
493
+ %Q{empty_#{ column.sql_type.downcase rescue 'blob' }()}
494
+ # NLS_DATE_FORMAT independent TIMESTAMP support
495
+ when :timestamp
496
+ quote_timestamp_with_to_timestamp(value)
497
+ # NLS_DATE_FORMAT independent DATE support
498
+ when :date, :time, :datetime
499
+ quote_date_with_to_date(value)
500
+ else
501
+ super
502
+ end
503
+ elsif value.acts_like?(:date)
504
+ quote_date_with_to_date(value)
505
+ elsif value.acts_like?(:time)
506
+ value.to_i == value.to_f ? quote_date_with_to_date(value) : quote_timestamp_with_to_timestamp(value)
507
+ else
508
+ super
509
+ end
510
+ end
511
+
512
+ def quoted_true #:nodoc:
513
+ return "'#{self.class.boolean_to_string(true)}'" if emulate_booleans_from_strings
514
+ "1"
515
+ end
516
+
517
+ def quoted_false #:nodoc:
518
+ return "'#{self.class.boolean_to_string(false)}'" if emulate_booleans_from_strings
519
+ "0"
520
+ end
521
+
522
+ def quote_date_with_to_date(value) #:nodoc:
523
+ # should support that composite_primary_keys gem will pass date as string
524
+ value = quoted_date(value) if value.acts_like?(:date) || value.acts_like?(:time)
525
+ "TO_DATE('#{value}','YYYY-MM-DD HH24:MI:SS')"
526
+ end
527
+
528
+ def quote_timestamp_with_to_timestamp(value) #:nodoc:
529
+ # add up to 9 digits of fractional seconds to inserted time
530
+ value = "#{quoted_date(value)}:#{("%.6f"%value.to_f).split('.')[1]}" if value.acts_like?(:time)
531
+ "TO_TIMESTAMP('#{value}','YYYY-MM-DD HH24:MI:SS:FF6')"
532
+ end
533
+
534
+ # CONNECTION MANAGEMENT ====================================
535
+ #
536
+
537
+ # If SQL statement fails due to lost connection then reconnect
538
+ # and retry SQL statement if autocommit mode is enabled.
539
+ # By default this functionality is disabled.
540
+ attr_reader :auto_retry #:nodoc:
541
+ @auto_retry = false
542
+
543
+ def auto_retry=(value) #:nodoc:
544
+ @auto_retry = value
545
+ @connection.auto_retry = value if @connection
546
+ end
547
+
548
+ # return raw OCI8 or JDBC connection
549
+ def raw_connection
550
+ @connection.raw_connection
551
+ end
552
+
553
+ # Returns true if the connection is active.
554
+ def active? #:nodoc:
555
+ # Pings the connection to check if it's still good. Note that an
556
+ # #active? method is also available, but that simply returns the
557
+ # last known state, which isn't good enough if the connection has
558
+ # gone stale since the last use.
559
+ @connection.ping
560
+ rescue OracleEnhancedConnectionException
561
+ false
562
+ end
563
+
564
+ # Reconnects to the database.
565
+ def reconnect! #:nodoc:
566
+ @connection.reset!
567
+ rescue OracleEnhancedConnectionException => e
568
+ @logger.warn "#{adapter_name} automatic reconnection failed: #{e.message}" if @logger
569
+ end
570
+
571
+ # Disconnects from the database.
572
+ def disconnect! #:nodoc:
573
+ @connection.logoff rescue nil
574
+ end
575
+
576
+ # DATABASE STATEMENTS ======================================
577
+ #
578
+ # see: abstract/database_statements.rb
579
+
580
+ # Executes a SQL statement
581
+ def execute(sql, name = nil)
582
+ # hack to pass additional "with_returning" option without changing argument list
583
+ log(sql, name) { sql.instance_variable_get(:@with_returning) ? @connection.exec_with_returning(sql) : @connection.exec(sql) }
584
+ end
585
+
586
+ # Returns an array of arrays containing the field values.
587
+ # Order is the same as that returned by #columns.
588
+ def select_rows(sql, name = nil)
589
+ # last parameter indicates to return also column list
590
+ result, columns = select(sql, name, true)
591
+ result.map{ |v| columns.map{|c| v[c]} }
592
+ end
593
+
594
+ # Executes an INSERT statement and returns the new record's ID
595
+ def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
596
+ # if primary key value is already prefetched from sequence
597
+ # or if there is no primary key
598
+ if id_value || pk.nil?
599
+ execute(sql, name)
600
+ return id_value
601
+ end
602
+
603
+ sql_with_returning = sql.dup << @connection.returning_clause(quote_column_name(pk))
604
+ # hack to pass additional "with_returning" option without changing argument list
605
+ sql_with_returning.instance_variable_set(:@with_returning, true)
606
+ clear_query_cache
607
+ execute(sql_with_returning, name)
608
+ end
609
+ protected :insert_sql
610
+
611
+ # use in set_sequence_name to avoid fetching primary key value from sequence
612
+ AUTOGENERATED_SEQUENCE_NAME = 'autogenerated'.freeze
613
+
614
+ # Returns the next sequence value from a sequence generator. Not generally
615
+ # called directly; used by ActiveRecord to get the next primary key value
616
+ # when inserting a new database record (see #prefetch_primary_key?).
617
+ def next_sequence_value(sequence_name)
618
+ # if sequence_name is set to :autogenerated then it means that primary key will be populated by trigger
619
+ return nil if sequence_name == AUTOGENERATED_SEQUENCE_NAME
620
+ select_one("SELECT #{quote_table_name(sequence_name)}.NEXTVAL id FROM dual")['id']
621
+ end
622
+
623
+ def begin_db_transaction #:nodoc:
624
+ @connection.autocommit = false
625
+ end
626
+
627
+ def commit_db_transaction #:nodoc:
628
+ @connection.commit
629
+ ensure
630
+ @connection.autocommit = true
631
+ end
632
+
633
+ def rollback_db_transaction #:nodoc:
634
+ @connection.rollback
635
+ ensure
636
+ @connection.autocommit = true
637
+ end
638
+
639
+ def create_savepoint #:nodoc:
640
+ execute("SAVEPOINT #{current_savepoint_name}")
641
+ end
642
+
643
+ def rollback_to_savepoint #:nodoc:
644
+ execute("ROLLBACK TO #{current_savepoint_name}")
645
+ end
646
+
647
+ def release_savepoint #:nodoc:
648
+ # there is no RELEASE SAVEPOINT statement in Oracle
649
+ end
650
+
651
+ def add_limit_offset!(sql, options) #:nodoc:
652
+ # added to_i for limit and offset to protect from SQL injection
653
+ offset = (options[:offset] || 0).to_i
654
+
655
+ if limit = options[:limit]
656
+ limit = limit.to_i
657
+ sql.replace "select * from (select raw_sql_.*, rownum raw_rnum_ from (#{sql}) raw_sql_ where rownum <= #{offset+limit}) where raw_rnum_ > #{offset}"
658
+ elsif offset > 0
659
+ sql.replace "select * from (select raw_sql_.*, rownum raw_rnum_ from (#{sql}) raw_sql_) where raw_rnum_ > #{offset}"
660
+ end
661
+ end
662
+
663
+ @@do_not_prefetch_primary_key = {}
664
+
665
+ # Returns true for Oracle adapter (since Oracle requires primary key
666
+ # values to be pre-fetched before insert). See also #next_sequence_value.
667
+ def prefetch_primary_key?(table_name = nil)
668
+ ! @@do_not_prefetch_primary_key[table_name.to_s]
669
+ end
670
+
671
+ # used just in tests to clear prefetch primary key flag for all tables
672
+ def clear_prefetch_primary_key #:nodoc:
673
+ @@do_not_prefetch_primary_key = {}
674
+ end
675
+
676
+ # Returns default sequence name for table.
677
+ # Will take all or first 26 characters of table name and append _seq suffix
678
+ def default_sequence_name(table_name, primary_key = nil)
679
+ # TODO: remove schema prefix if present before truncating
680
+ # truncate table name if necessary to fit in max length of identifier
681
+ "#{table_name.to_s[0,IDENTIFIER_MAX_LENGTH-4]}_seq"
682
+ end
683
+
684
+ # Inserts the given fixture into the table. Overridden to properly handle lobs.
685
+ def insert_fixture(fixture, table_name) #:nodoc:
686
+ super
687
+
688
+ klass = fixture.class_name.constantize rescue nil
689
+ if klass.respond_to?(:ancestors) && klass.ancestors.include?(ActiveRecord::Base)
690
+ write_lobs(table_name, klass, fixture)
691
+ end
692
+ end
693
+
694
+ # Writes LOB values from attributes, as indicated by the LOB columns of klass.
695
+ def write_lobs(table_name, klass, attributes) #:nodoc:
696
+ # is class with composite primary key>
697
+ is_with_cpk = klass.respond_to?(:composite?) && klass.composite?
698
+ if is_with_cpk
699
+ id = klass.primary_key.map {|pk| attributes[pk.to_s] }
700
+ else
701
+ id = quote(attributes[klass.primary_key])
702
+ end
703
+ klass.columns.select { |col| col.sql_type =~ /LOB$/i }.each do |col|
704
+ value = attributes[col.name]
705
+ # changed sequence of next two lines - should check if value is nil before converting to yaml
706
+ next if value.nil? || (value == '')
707
+ value = value.to_yaml if col.text? && klass.serialized_attributes[col.name]
708
+ uncached do
709
+ if is_with_cpk
710
+ lob = select_one("SELECT #{col.name} FROM #{table_name} WHERE #{klass.composite_where_clause(id)} FOR UPDATE",
711
+ 'Writable Large Object')[col.name]
712
+ else
713
+ lob = select_one("SELECT #{col.name} FROM #{table_name} WHERE #{klass.primary_key} = #{id} FOR UPDATE",
714
+ 'Writable Large Object')[col.name]
715
+ end
716
+ @connection.write_lob(lob, value.to_s, col.type == :binary)
717
+ end
718
+ end
719
+ end
720
+
721
+ # change LOB column for ORDER BY clause
722
+ # just first 100 characters are taken for ordering
723
+ def lob_order_by_expression(klass, order) #:nodoc:
724
+ return order if order.nil?
725
+ changed = false
726
+ new_order = order.to_s.strip.split(/, */).map do |order_by_col|
727
+ column_name, asc_desc = order_by_col.split(/ +/)
728
+ if column = klass.columns.detect { |col| col.name == column_name && col.sql_type =~ /LOB$/i}
729
+ changed = true
730
+ "DBMS_LOB.SUBSTR(#{column_name},100,1) #{asc_desc}"
731
+ else
732
+ order_by_col
733
+ end
734
+ end.join(', ')
735
+ changed ? new_order : order
736
+ end
737
+
738
+ # SCHEMA STATEMENTS ========================================
739
+ #
740
+ # see: abstract/schema_statements.rb
741
+
742
+ # Current database name
743
+ def current_database
744
+ select_value("select sys_context('userenv','db_name') from dual")
745
+ end
746
+
747
+ # Current database session user
748
+ def current_user
749
+ select_value("select sys_context('userenv','session_user') from dual")
750
+ end
751
+
752
+ # Default tablespace name of current user
753
+ def default_tablespace
754
+ select_value("select lower(default_tablespace) from user_users where username = sys_context('userenv','session_user')")
755
+ end
756
+
757
+ def tables(name = nil) #:nodoc:
758
+ # changed select from user_tables to all_tables - much faster in large data dictionaries
759
+ 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']}
760
+ end
761
+
762
+ cattr_accessor :all_schema_indexes #:nodoc:
763
+
764
+ # This method selects all indexes at once, and caches them in a class variable.
765
+ # Subsequent index calls get them from the variable, without going to the DB.
766
+ def indexes(table_name, name = nil) #:nodoc:
767
+ (owner, table_name, db_link) = @connection.describe(table_name)
768
+ unless all_schema_indexes
769
+ default_tablespace_name = default_tablespace
770
+ result = select_all(<<-SQL)
771
+ SELECT lower(i.table_name) as table_name, lower(i.index_name) as index_name, i.uniqueness, lower(i.tablespace_name) as tablespace_name, lower(c.column_name) as column_name, e.column_expression as column_expression
772
+ FROM all_indexes#{db_link} i
773
+ JOIN all_ind_columns#{db_link} c on c.index_name = i.index_name and c.index_owner = i.owner
774
+ LEFT OUTER JOIN all_ind_expressions#{db_link} e on e.index_name = i.index_name and e.index_owner = i.owner and e.column_position = c.column_position
775
+ WHERE i.owner = '#{owner}'
776
+ AND i.table_owner = '#{owner}'
777
+ 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')
778
+ ORDER BY i.index_name, c.column_position
779
+ SQL
780
+
781
+ current_index = nil
782
+ self.all_schema_indexes = []
783
+
784
+ result.each do |row|
785
+ # have to keep track of indexes because above query returns dups
786
+ # there is probably a better query we could figure out
787
+ if current_index != row['index_name']
788
+ all_schema_indexes << OracleEnhancedIndexDefinition.new(row['table_name'], row['index_name'], row['uniqueness'] == "UNIQUE",
789
+ row['tablespace_name'] == default_tablespace_name ? nil : row['tablespace_name'], [])
790
+ current_index = row['index_name']
791
+ end
792
+ all_schema_indexes.last.columns << (row['column_expression'].nil? ? row['column_name'] : row['column_expression'].gsub('"','').downcase)
793
+ end
794
+ end
795
+
796
+ # Return the indexes just for the requested table, since AR is structured that way
797
+ table_name = table_name.downcase
798
+ all_schema_indexes.select{|i| i.table == table_name}
799
+ end
800
+
801
+ @@ignore_table_columns = nil #:nodoc:
802
+
803
+ # set ignored columns for table
804
+ def ignore_table_columns(table_name, *args) #:nodoc:
805
+ @@ignore_table_columns ||= {}
806
+ @@ignore_table_columns[table_name] ||= []
807
+ @@ignore_table_columns[table_name] += args.map{|a| a.to_s.downcase}
808
+ @@ignore_table_columns[table_name].uniq!
809
+ end
810
+
811
+ def ignored_table_columns(table_name) #:nodoc:
812
+ @@ignore_table_columns ||= {}
813
+ @@ignore_table_columns[table_name]
814
+ end
815
+
816
+ # used just in tests to clear ignored table columns
817
+ def clear_ignored_table_columns #:nodoc:
818
+ @@ignore_table_columns = nil
819
+ end
820
+
821
+ @@table_column_type = nil #:nodoc:
822
+
823
+ # set explicit type for specified table columns
824
+ def set_type_for_columns(table_name, column_type, *args) #:nodoc:
825
+ @@table_column_type ||= {}
826
+ @@table_column_type[table_name] ||= {}
827
+ args.each do |col|
828
+ @@table_column_type[table_name][col.to_s.downcase] = column_type
829
+ end
830
+ end
831
+
832
+ def get_type_for_column(table_name, column_name) #:nodoc:
833
+ @@table_column_type && @@table_column_type[table_name] && @@table_column_type[table_name][column_name.to_s.downcase]
834
+ end
835
+
836
+ # used just in tests to clear column data type definitions
837
+ def clear_types_for_columns #:nodoc:
838
+ @@table_column_type = nil
839
+ end
840
+
841
+ # check if table has primary key trigger with _pkt suffix
842
+ def has_primary_key_trigger?(table_name, owner = nil, desc_table_name = nil, db_link = nil)
843
+ (owner, desc_table_name, db_link) = @connection.describe(table_name) unless owner
844
+
845
+ trigger_name = default_trigger_name(table_name).upcase
846
+ pkt_sql = <<-SQL
847
+ SELECT trigger_name
848
+ FROM all_triggers#{db_link}
849
+ WHERE owner = '#{owner}'
850
+ AND trigger_name = '#{trigger_name}'
851
+ AND table_owner = '#{owner}'
852
+ AND table_name = '#{desc_table_name}'
853
+ AND status = 'ENABLED'
854
+ SQL
855
+ select_value(pkt_sql) ? true : false
856
+ end
857
+
858
+ ##
859
+ # :singleton-method:
860
+ # Cache column description between requests.
861
+ # Could be used in development environment to avoid selecting table columns from data dictionary tables for each request.
862
+ # This can speed up request processing in development mode if development database is not on local computer.
863
+ #
864
+ # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.cache_columns = true
865
+ cattr_accessor :cache_columns
866
+ self.cache_columns = false
867
+
868
+ def columns(table_name, name = nil) #:nodoc:
869
+ # Don't double cache if config.cache_classes is turned on
870
+ if @@cache_columns && !(defined?(Rails) && Rails.configuration.cache_classes)
871
+ @@columns_cache ||= {}
872
+ @@columns_cache[table_name] ||= columns_without_cache(table_name, name)
873
+ else
874
+ columns_without_cache(table_name, name)
875
+ end
876
+ end
877
+
878
+ def columns_without_cache(table_name, name = nil) #:nodoc:
879
+ # get ignored_columns by original table name
880
+ ignored_columns = ignored_table_columns(table_name)
881
+
882
+ (owner, desc_table_name, db_link) = @connection.describe(table_name)
883
+
884
+ if has_primary_key_trigger?(table_name, owner, desc_table_name, db_link)
885
+ @@do_not_prefetch_primary_key[table_name] = true
886
+ end
887
+
888
+ table_cols = <<-SQL
889
+ select column_name as name, data_type as sql_type, data_default, nullable,
890
+ decode(data_type, 'NUMBER', data_precision,
891
+ 'FLOAT', data_precision,
892
+ 'VARCHAR2', decode(char_used, 'C', char_length, data_length),
893
+ 'CHAR', decode(char_used, 'C', char_length, data_length),
894
+ null) as limit,
895
+ decode(data_type, 'NUMBER', data_scale, null) as scale
896
+ from all_tab_columns#{db_link}
897
+ where owner = '#{owner}'
898
+ and table_name = '#{desc_table_name}'
899
+ order by column_id
900
+ SQL
901
+
902
+ # added deletion of ignored columns
903
+ select_all(table_cols, name).delete_if do |row|
904
+ ignored_columns && ignored_columns.include?(row['name'].downcase)
905
+ end.map do |row|
906
+ limit, scale = row['limit'], row['scale']
907
+ if limit || scale
908
+ row['sql_type'] << "(#{(limit || 38).to_i}" + ((scale = scale.to_i) > 0 ? ",#{scale})" : ")")
909
+ end
910
+
911
+ # clean up odd default spacing from Oracle
912
+ if row['data_default']
913
+ row['data_default'].sub!(/^(.*?)\s*$/, '\1')
914
+
915
+ # If a default contains a newline these cleanup regexes need to
916
+ # match newlines.
917
+ row['data_default'].sub!(/^'(.*)'$/m, '\1')
918
+ row['data_default'] = nil if row['data_default'] =~ /^(null|empty_[bc]lob\(\))$/i
919
+ end
920
+
921
+ OracleEnhancedColumn.new(oracle_downcase(row['name']),
922
+ row['data_default'],
923
+ row['sql_type'],
924
+ row['nullable'] == 'Y',
925
+ # pass table name for table specific column definitions
926
+ table_name,
927
+ # pass column type if specified in class definition
928
+ get_type_for_column(table_name, oracle_downcase(row['name'])))
929
+ end
930
+ end
931
+
932
+ # used just in tests to clear column cache
933
+ def clear_columns_cache #:nodoc:
934
+ @@columns_cache = nil
935
+ end
936
+
937
+ # used in migrations to clear column cache for specified table
938
+ def clear_table_columns_cache(table_name)
939
+ @@columns_cache[table_name.to_s] = nil if @@cache_columns
940
+ end
941
+
942
+ ##
943
+ # :singleton-method:
944
+ # Specify default sequence start with value (by default 10000 if not explicitly set), e.g.:
945
+ #
946
+ # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.default_sequence_start_value = 1
947
+ cattr_accessor :default_sequence_start_value
948
+ self.default_sequence_start_value = 10000
949
+
950
+ # Additional options for +create_table+ method in migration files.
951
+ #
952
+ # You can specify individual starting value in table creation migration file, e.g.:
953
+ #
954
+ # create_table :users, :sequence_start_value => 100 do |t|
955
+ # # ...
956
+ # end
957
+ #
958
+ # You can also specify other sequence definition additional parameters, e.g.:
959
+ #
960
+ # create_table :users, :sequence_start_value => “100 NOCACHE INCREMENT BY 10” do |t|
961
+ # # ...
962
+ # end
963
+ #
964
+ # Create primary key trigger (so that you can skip primary key value in INSERT statement).
965
+ # By default trigger name will be "table_name_pkt", you can override the name with
966
+ # :trigger_name option (but it is not recommended to override it as then this trigger will
967
+ # not be detected by ActiveRecord model and it will still do prefetching of sequence value).
968
+ # Example:
969
+ #
970
+ # create_table :users, :primary_key_trigger => true do |t|
971
+ # # ...
972
+ # end
973
+ #
974
+ # It is possible to add table and column comments in table creation migration files:
975
+ #
976
+ # create_table :employees, :comment => “Employees and contractors” do |t|
977
+ # t.string :first_name, :comment => “Given name”
978
+ # t.string :last_name, :comment => “Surname”
979
+ # end
980
+
981
+ def create_table(name, options = {}, &block)
982
+ create_sequence = options[:id] != false
983
+ column_comments = {}
984
+
985
+ table_definition = TableDefinition.new(self)
986
+ table_definition.primary_key(options[:primary_key] || Base.get_primary_key(name.to_s.singularize)) unless options[:id] == false
987
+
988
+ # store that primary key was defined in create_table block
989
+ unless create_sequence
990
+ class << table_definition
991
+ attr_accessor :create_sequence
992
+ def primary_key(*args)
993
+ self.create_sequence = true
994
+ super(*args)
995
+ end
996
+ end
997
+ end
998
+
999
+ # store column comments
1000
+ class << table_definition
1001
+ attr_accessor :column_comments
1002
+ def column(name, type, options = {})
1003
+ if options[:comment]
1004
+ self.column_comments ||= {}
1005
+ self.column_comments[name] = options[:comment]
1006
+ end
1007
+ super(name, type, options)
1008
+ end
1009
+ end
1010
+
1011
+ result = block.call(table_definition) if block
1012
+ create_sequence = create_sequence || table_definition.create_sequence
1013
+ column_comments = table_definition.column_comments if table_definition.column_comments
1014
+
1015
+
1016
+ if options[:force] && table_exists?(name)
1017
+ drop_table(name, options)
1018
+ end
1019
+
1020
+ create_sql = "CREATE#{' GLOBAL TEMPORARY' if options[:temporary]} TABLE "
1021
+ create_sql << "#{quote_table_name(name)} ("
1022
+ create_sql << table_definition.to_sql
1023
+ create_sql << ") #{options[:options]}"
1024
+ execute create_sql
1025
+
1026
+ create_sequence_and_trigger(name, options) if create_sequence
1027
+
1028
+ add_table_comment name, options[:comment]
1029
+ column_comments.each do |column_name, comment|
1030
+ add_comment name, column_name, comment
1031
+ end
1032
+
1033
+ end
1034
+
1035
+ def rename_table(name, new_name) #:nodoc:
1036
+ execute "RENAME #{quote_table_name(name)} TO #{quote_table_name(new_name)}"
1037
+ execute "RENAME #{quote_table_name("#{name}_seq")} TO #{quote_table_name("#{new_name}_seq")}" rescue nil
1038
+ end
1039
+
1040
+ def drop_table(name, options = {}) #:nodoc:
1041
+ super(name)
1042
+ seq_name = options[:sequence_name] || default_sequence_name(name)
1043
+ execute "DROP SEQUENCE #{quote_table_name(seq_name)}" rescue nil
1044
+ ensure
1045
+ clear_table_columns_cache(name)
1046
+ end
1047
+
1048
+ # clear cached indexes when adding new index
1049
+ def add_index(table_name, column_name, options = {}) #:nodoc:
1050
+ self.all_schema_indexes = nil
1051
+ column_names = Array(column_name)
1052
+ index_name = index_name(table_name, :column => column_names)
1053
+
1054
+ if Hash === options # legacy support, since this param was a string
1055
+ index_type = options[:unique] ? "UNIQUE" : ""
1056
+ index_name = options[:name] || index_name
1057
+ tablespace = if options[:tablespace]
1058
+ " TABLESPACE #{options[:tablespace]}"
1059
+ else
1060
+ ""
1061
+ end
1062
+ else
1063
+ index_type = options
1064
+ end
1065
+ quoted_column_names = column_names.map { |e| quote_column_name(e) }.join(", ")
1066
+ execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{quoted_column_names})#{tablespace}"
1067
+ end
1068
+
1069
+ # clear cached indexes when removing index
1070
+ def remove_index(table_name, options = {}) #:nodoc:
1071
+ self.all_schema_indexes = nil
1072
+ execute "DROP INDEX #{index_name(table_name, options)}"
1073
+ end
1074
+
1075
+ # returned shortened index name if default is too large
1076
+ def index_name(table_name, options) #:nodoc:
1077
+ default_name = super(table_name, options)
1078
+ return default_name if default_name.length <= IDENTIFIER_MAX_LENGTH
1079
+
1080
+ # remove 'index', 'on' and 'and' keywords
1081
+ shortened_name = "i_#{table_name}_#{Array(options[:column]) * '_'}"
1082
+
1083
+ # leave just first three letters from each word
1084
+ if shortened_name.length > IDENTIFIER_MAX_LENGTH
1085
+ shortened_name = shortened_name.split('_').map{|w| w[0,3]}.join('_')
1086
+ end
1087
+ # generate unique name using hash function
1088
+ if shortened_name.length > OracleEnhancedAdapter::IDENTIFIER_MAX_LENGTH
1089
+ shortened_name = 'i'+Digest::SHA1.hexdigest(default_name)[0,OracleEnhancedAdapter::IDENTIFIER_MAX_LENGTH-1]
1090
+ end
1091
+ @logger.warn "#{adapter_name} shortened default index name #{default_name} to #{shortened_name}" if @logger
1092
+ shortened_name
1093
+ end
1094
+
1095
+ def add_column(table_name, column_name, type, options = {}) #:nodoc:
1096
+ 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])}"
1097
+ options[:type] = type
1098
+ add_column_options!(add_column_sql, options)
1099
+ execute(add_column_sql)
1100
+ ensure
1101
+ clear_table_columns_cache(table_name)
1102
+ end
1103
+
1104
+ def change_column_default(table_name, column_name, default) #:nodoc:
1105
+ execute "ALTER TABLE #{quote_table_name(table_name)} MODIFY #{quote_column_name(column_name)} DEFAULT #{quote(default)}"
1106
+ ensure
1107
+ clear_table_columns_cache(table_name)
1108
+ end
1109
+
1110
+ def change_column_null(table_name, column_name, null, default = nil) #:nodoc:
1111
+ column = column_for(table_name, column_name)
1112
+
1113
+ unless null || default.nil?
1114
+ execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
1115
+ end
1116
+
1117
+ change_column table_name, column_name, column.sql_type, :null => null
1118
+ end
1119
+
1120
+ def change_column(table_name, column_name, type, options = {}) #:nodoc:
1121
+ column = column_for(table_name, column_name)
1122
+
1123
+ # remove :null option if its value is the same as current column definition
1124
+ # otherwise Oracle will raise error
1125
+ if options.has_key?(:null) && options[:null] == column.null
1126
+ options[:null] = nil
1127
+ end
1128
+
1129
+ 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])}"
1130
+ options[:type] = type
1131
+ add_column_options!(change_column_sql, options)
1132
+ execute(change_column_sql)
1133
+ ensure
1134
+ clear_table_columns_cache(table_name)
1135
+ end
1136
+
1137
+ def rename_column(table_name, column_name, new_column_name) #:nodoc:
1138
+ execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} to #{quote_column_name(new_column_name)}"
1139
+ ensure
1140
+ clear_table_columns_cache(table_name)
1141
+ end
1142
+
1143
+ def remove_column(table_name, column_name) #:nodoc:
1144
+ execute "ALTER TABLE #{quote_table_name(table_name)} DROP COLUMN #{quote_column_name(column_name)}"
1145
+ ensure
1146
+ clear_table_columns_cache(table_name)
1147
+ end
1148
+
1149
+ def add_comment(table_name, column_name, comment) #:nodoc:
1150
+ return if comment.blank?
1151
+ execute "COMMENT ON COLUMN #{quote_table_name(table_name)}.#{column_name} IS '#{comment}'"
1152
+ end
1153
+
1154
+ def add_table_comment(table_name, comment) #:nodoc:
1155
+ return if comment.blank?
1156
+ execute "COMMENT ON TABLE #{quote_table_name(table_name)} IS '#{comment}'"
1157
+ end
1158
+
1159
+ def table_comment(table_name) #:nodoc:
1160
+ (owner, table_name, db_link) = @connection.describe(table_name)
1161
+ select_value <<-SQL
1162
+ SELECT comments FROM all_tab_comments#{db_link}
1163
+ WHERE owner = '#{owner}'
1164
+ AND table_name = '#{table_name}'
1165
+ SQL
1166
+ end
1167
+
1168
+ def column_comment(table_name, column_name) #:nodoc:
1169
+ (owner, table_name, db_link) = @connection.describe(table_name)
1170
+ select_value <<-SQL
1171
+ SELECT comments FROM all_col_comments#{db_link}
1172
+ WHERE owner = '#{owner}'
1173
+ AND table_name = '#{table_name}'
1174
+ AND column_name = '#{column_name.upcase}'
1175
+ SQL
1176
+ end
1177
+
1178
+ # Maps logical Rails types to Oracle-specific data types.
1179
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
1180
+ # Ignore options for :text and :binary columns
1181
+ return super(type, nil, nil, nil) if ['text', 'binary'].include?(type.to_s)
1182
+
1183
+ super
1184
+ end
1185
+
1186
+ # Find a table's primary key and sequence.
1187
+ # *Note*: Only primary key is implemented - sequence will be nil.
1188
+ def pk_and_sequence_for(table_name) #:nodoc:
1189
+ (owner, table_name, db_link) = @connection.describe(table_name)
1190
+
1191
+ # changed select from all_constraints to user_constraints - much faster in large data dictionaries
1192
+ pks = select_values(<<-SQL, 'Primary Key')
1193
+ select cc.column_name
1194
+ from user_constraints#{db_link} c, user_cons_columns#{db_link} cc
1195
+ where c.owner = '#{owner}'
1196
+ and c.table_name = '#{table_name}'
1197
+ and c.constraint_type = 'P'
1198
+ and cc.owner = c.owner
1199
+ and cc.constraint_name = c.constraint_name
1200
+ SQL
1201
+
1202
+ # only support single column keys
1203
+ pks.size == 1 ? [oracle_downcase(pks.first), nil] : nil
1204
+ end
1205
+
1206
+ def structure_dump #:nodoc:
1207
+ s = select_all("select sequence_name from user_sequences order by 1").inject("") do |structure, seq|
1208
+ structure << "create sequence #{seq.to_a.first.last}#{STATEMENT_TOKEN}"
1209
+ end
1210
+
1211
+ # changed select from user_tables to all_tables - much faster in large data dictionaries
1212
+ select_all("select table_name from all_tables where owner = sys_context('userenv','session_user') order by 1").inject(s) do |structure, table|
1213
+ table_name = table['table_name']
1214
+ virtual_columns = virtual_columns_for(table_name)
1215
+ ddl = "create#{ ' global temporary' if temporary_table?(table_name)} table #{table_name} (\n "
1216
+ cols = select_all(%Q{
1217
+ select column_name, data_type, data_length, char_used, char_length, data_precision, data_scale, data_default, nullable
1218
+ from user_tab_columns
1219
+ where table_name = '#{table_name}'
1220
+ order by column_id
1221
+ }).map do |row|
1222
+ if(v = virtual_columns.find {|col| col['column_name'] == row['column_name']})
1223
+ structure_dump_virtual_column(row, v['data_default'])
1224
+ else
1225
+ structure_dump_column(row)
1226
+ end
1227
+ end
1228
+ ddl << cols.join(",\n ")
1229
+ ddl << structure_dump_constraints(table_name)
1230
+ ddl << "\n)#{STATEMENT_TOKEN}"
1231
+ structure << ddl
1232
+ structure << structure_dump_indexes(table_name)
1233
+ end
1234
+ end
1235
+
1236
+ def structure_dump_virtual_column(column, data_default) #:nodoc:
1237
+ data_default = data_default.gsub(/"/, '')
1238
+ col = "#{column['column_name'].downcase} #{column['data_type'].downcase}"
1239
+ if column['data_type'] =='NUMBER' and !column['data_precision'].nil?
1240
+ col << "(#{column['data_precision'].to_i}"
1241
+ col << ",#{column['data_scale'].to_i}" if !column['data_scale'].nil?
1242
+ col << ')'
1243
+ elsif column['data_type'].include?('CHAR')
1244
+ length = column['char_used'] == 'C' ? column['char_length'].to_i : column['data_length'].to_i
1245
+ col << "(#{length})"
1246
+ end
1247
+ col << " GENERATED ALWAYS AS (#{data_default}) VIRTUAL"
1248
+ end
1249
+
1250
+ def structure_dump_column(column) #:nodoc:
1251
+ col = "#{column['column_name'].downcase} #{column['data_type'].downcase}"
1252
+ if column['data_type'] =='NUMBER' and !column['data_precision'].nil?
1253
+ col << "(#{column['data_precision'].to_i}"
1254
+ col << ",#{column['data_scale'].to_i}" if !column['data_scale'].nil?
1255
+ col << ')'
1256
+ elsif column['data_type'].include?('CHAR')
1257
+ length = column['char_used'] == 'C' ? column['char_length'].to_i : column['data_length'].to_i
1258
+ col << "(#{length})"
1259
+ end
1260
+ col << " default #{column['data_default']}" if !column['data_default'].nil?
1261
+ col << ' not null' if column['nullable'] == 'N'
1262
+ col
1263
+ end
1264
+
1265
+ def structure_dump_constraints(table) #:nodoc:
1266
+ out = [structure_dump_primary_key(table), structure_dump_unique_keys(table)].flatten.compact
1267
+ out.length > 0 ? ",\n#{out.join(",\n")}" : ''
1268
+ end
1269
+
1270
+ def structure_dump_primary_key(table) #:nodoc:
1271
+ opts = {:name => '', :cols => []}
1272
+ pks = select_all(<<-SQL, "Primary Keys")
1273
+ select a.constraint_name, a.column_name, a.position
1274
+ from user_cons_columns a
1275
+ join user_constraints c
1276
+ on a.constraint_name = c.constraint_name
1277
+ where c.table_name = '#{table.upcase}'
1278
+ and c.constraint_type = 'P'
1279
+ and c.owner = sys_context('userenv', 'session_user')
1280
+ SQL
1281
+ pks.each do |row|
1282
+ opts[:name] = row['constraint_name']
1283
+ opts[:cols][row['position']-1] = row['column_name']
1284
+ end
1285
+ opts[:cols].length > 0 ? " CONSTRAINT #{opts[:name]} PRIMARY KEY (#{opts[:cols].join(',')})" : nil
1286
+ end
1287
+
1288
+ def structure_dump_unique_keys(table) #:nodoc:
1289
+ keys = {}
1290
+ uks = select_all(<<-SQL, "Primary Keys")
1291
+ select a.constraint_name, a.column_name, a.position
1292
+ from user_cons_columns a
1293
+ join user_constraints c
1294
+ on a.constraint_name = c.constraint_name
1295
+ where c.table_name = '#{table.upcase}'
1296
+ and c.constraint_type = 'U'
1297
+ and c.owner = sys_context('userenv', 'session_user')
1298
+ SQL
1299
+ uks.each do |uk|
1300
+ keys[uk['constraint_name']] ||= []
1301
+ keys[uk['constraint_name']][uk['position']-1] = uk['column_name']
1302
+ end
1303
+ keys.map do |k,v|
1304
+ " CONSTRAINT #{k} UNIQUE (#{v.join(',')})"
1305
+ end
1306
+ end
1307
+
1308
+ def structure_dump_fk_constraints #:nodoc:
1309
+ fks = select_all("select table_name from all_tables where owner = sys_context('userenv','session_user') order by 1").map do |table|
1310
+ if respond_to?(:foreign_keys) && (foreign_keys = foreign_keys(table["table_name"])).any?
1311
+ foreign_keys.map do |fk|
1312
+ column = fk.options[:column] || "#{fk.to_table.to_s.singularize}_id"
1313
+ constraint_name = foreign_key_constraint_name(fk.from_table, column, fk.options)
1314
+ sql = "ALTER TABLE #{quote_table_name(fk.from_table)} ADD CONSTRAINT #{quote_column_name(constraint_name)} "
1315
+ sql << "#{foreign_key_definition(fk.to_table, fk.options)}"
1316
+ end
1317
+ end
1318
+ end.flatten.compact.join(STATEMENT_TOKEN)
1319
+ fks.length > 1 ? "#{fks}#{STATEMENT_TOKEN}" : ''
1320
+ end
1321
+
1322
+ # Extract all stored procedures, packages, synonyms and views.
1323
+ def structure_dump_db_stored_code #:nodoc:
1324
+ structure = ""
1325
+ select_all("select distinct name, type
1326
+ from all_source
1327
+ where type in ('PROCEDURE', 'PACKAGE', 'PACKAGE BODY', 'FUNCTION', 'TRIGGER', 'TYPE')
1328
+ and owner = sys_context('userenv','session_user') order by type").each do |source|
1329
+ ddl = "create or replace \n "
1330
+ lines = select_all(%Q{
1331
+ select text
1332
+ from all_source
1333
+ where name = '#{source['name']}'
1334
+ and type = '#{source['type']}'
1335
+ and owner = sys_context('userenv','session_user')
1336
+ order by line
1337
+ }).map do |row|
1338
+ ddl << row['text'] if row['text'].size > 1
1339
+ end
1340
+ ddl << ";" unless ddl.strip.last == ";"
1341
+ structure << ddl << STATEMENT_TOKEN
1342
+ end
1343
+
1344
+ # export views
1345
+ select_all("select view_name, text from user_views").each do |view|
1346
+ ddl = "create or replace view #{view['view_name']} AS\n "
1347
+ # any views with empty lines will cause OCI to barf when loading. remove blank lines =/
1348
+ ddl << view['text'].gsub(/^\n/, '')
1349
+ structure << ddl << STATEMENT_TOKEN
1350
+ end
1351
+
1352
+ # export synonyms
1353
+ select_all("select owner, synonym_name, table_name, table_owner
1354
+ from all_synonyms
1355
+ where owner = sys_context('userenv','session_user') ").each do |synonym|
1356
+ ddl = "create or replace #{synonym['owner'] == 'PUBLIC' ? 'PUBLIC' : '' } SYNONYM #{synonym['synonym_name']} for #{synonym['table_owner']}.#{synonym['table_name']}"
1357
+ structure << ddl << STATEMENT_TOKEN
1358
+ end
1359
+
1360
+ structure
1361
+ end
1362
+
1363
+ def structure_dump_indexes(table_name) #:nodoc:
1364
+ statements = indexes(table_name).map do |options|
1365
+ #def add_index(table_name, column_name, options = {})
1366
+ column_names = options[:columns]
1367
+ options = {:name => options[:name], :unique => options[:unique]}
1368
+ index_name = index_name(table_name, :column => column_names)
1369
+ if Hash === options # legacy support, since this param was a string
1370
+ index_type = options[:unique] ? "UNIQUE" : ""
1371
+ index_name = options[:name] || index_name
1372
+ else
1373
+ index_type = options
1374
+ end
1375
+ quoted_column_names = column_names.map { |e| quote_column_name(e) }.join(", ")
1376
+ "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{quoted_column_names})"
1377
+ end
1378
+ statements.length > 0 ? "#{statements.join(STATEMENT_TOKEN)}#{STATEMENT_TOKEN}" : ''
1379
+ end
1380
+
1381
+ def structure_drop #:nodoc:
1382
+ s = select_all("select sequence_name from user_sequences order by 1").inject("") do |drop, seq|
1383
+ drop << "drop sequence #{seq.to_a.first.last};\n\n"
1384
+ end
1385
+
1386
+ # changed select from user_tables to all_tables - much faster in large data dictionaries
1387
+ select_all("select table_name from all_tables where owner = sys_context('userenv','session_user') order by 1").inject(s) do |drop, table|
1388
+ drop << "drop table #{table.to_a.first.last} cascade constraints;\n\n"
1389
+ end
1390
+ end
1391
+
1392
+ def temp_table_drop #:nodoc:
1393
+ # changed select from user_tables to all_tables - much faster in large data dictionaries
1394
+ select_all("select table_name from all_tables where owner = sys_context('userenv','session_user') and temporary = 'Y' order by 1").inject('') do |drop, table|
1395
+ drop << "drop table #{table.to_a.first.last} cascade constraints;\n\n"
1396
+ end
1397
+ end
1398
+
1399
+ def full_drop(preserve_tables=false) #:nodoc:
1400
+ s = preserve_tables ? [] : [structure_drop]
1401
+ s << temp_table_drop if preserve_tables
1402
+ s << drop_sql_for_feature("view")
1403
+ s << drop_sql_for_feature("synonym")
1404
+ s << drop_sql_for_feature("type")
1405
+ s << drop_sql_for_object("package")
1406
+ s << drop_sql_for_object("function")
1407
+ s << drop_sql_for_object("procedure")
1408
+ s.join("\n\n")
1409
+ end
1410
+
1411
+ def add_column_options!(sql, options) #:nodoc:
1412
+ type = options[:type] || ((column = options[:column]) && column.type)
1413
+ type = type && type.to_sym
1414
+ # handle case of defaults for CLOB columns, which would otherwise get "quoted" incorrectly
1415
+ if options_include_default?(options)
1416
+ if type == :text
1417
+ sql << " DEFAULT #{quote(options[:default])}"
1418
+ else
1419
+ # from abstract adapter
1420
+ sql << " DEFAULT #{quote(options[:default], options[:column])}"
1421
+ end
1422
+ end
1423
+ # must explicitly add NULL or NOT NULL to allow change_column to work on migrations
1424
+ if options[:null] == false
1425
+ sql << " NOT NULL"
1426
+ elsif options[:null] == true
1427
+ sql << " NULL" unless type == :primary_key
1428
+ end
1429
+ end
1430
+
1431
+ # SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
1432
+ #
1433
+ # Oracle requires the ORDER BY columns to be in the SELECT list for DISTINCT
1434
+ # queries. However, with those columns included in the SELECT DISTINCT list, you
1435
+ # won't actually get a distinct list of the column you want (presuming the column
1436
+ # has duplicates with multiple values for the ordered-by columns. So we use the
1437
+ # FIRST_VALUE function to get a single (first) value for each column, effectively
1438
+ # making every row the same.
1439
+ #
1440
+ # distinct("posts.id", "posts.created_at desc")
1441
+ def distinct(columns, order_by) #:nodoc:
1442
+ return "DISTINCT #{columns}" if order_by.blank?
1443
+
1444
+ # construct a valid DISTINCT clause, ie. one that includes the ORDER BY columns, using
1445
+ # FIRST_VALUE such that the inclusion of these columns doesn't invalidate the DISTINCT
1446
+ order_columns = order_by.split(',').map { |s| s.strip }.reject(&:blank?)
1447
+ order_columns = order_columns.zip((0...order_columns.size).to_a).map do |c, i|
1448
+ "FIRST_VALUE(#{c.split.first}) OVER (PARTITION BY #{columns} ORDER BY #{c}) AS alias_#{i}__"
1449
+ end
1450
+ sql = "DISTINCT #{columns}, "
1451
+ sql << order_columns * ", "
1452
+ end
1453
+
1454
+ def temporary_table?(table_name) #:nodoc:
1455
+ select_value("select temporary from user_tables where table_name = '#{table_name.upcase}'") == 'Y'
1456
+ end
1457
+
1458
+ # statements separator used in structure dump
1459
+ STATEMENT_TOKEN = "\n\n--@@@--\n\n"
1460
+
1461
+ # ORDER BY clause for the passed order option.
1462
+ #
1463
+ # Uses column aliases as defined by #distinct.
1464
+ def add_order_by_for_association_limiting!(sql, options) #:nodoc:
1465
+ return sql if options[:order].blank?
1466
+
1467
+ order = options[:order].split(',').collect { |s| s.strip }.reject(&:blank?)
1468
+ order.map! {|s| $1 if s =~ / (.*)/}
1469
+ order = order.zip((0...order.size).to_a).map { |s,i| "alias_#{i}__ #{s}" }.join(', ')
1470
+
1471
+ sql << " ORDER BY #{order}"
1472
+ end
1473
+
1474
+ protected
1475
+
1476
+ def translate_exception(exception, message) #:nodoc:
1477
+ case @connection.error_code(exception)
1478
+ when 1
1479
+ RecordNotUnique.new(message, exception)
1480
+ when 2291
1481
+ InvalidForeignKey.new(message, exception)
1482
+ else
1483
+ super
1484
+ end
1485
+ end
1486
+
1487
+ private
1488
+
1489
+ def select(sql, name = nil, return_column_names = false)
1490
+ log(sql, name) do
1491
+ @connection.select(sql, name, return_column_names)
1492
+ end
1493
+ end
1494
+
1495
+ def oracle_downcase(column_name)
1496
+ @connection.oracle_downcase(column_name)
1497
+ end
1498
+
1499
+ def column_for(table_name, column_name)
1500
+ unless column = columns(table_name).find { |c| c.name == column_name.to_s }
1501
+ raise "No such column: #{table_name}.#{column_name}"
1502
+ end
1503
+ column
1504
+ end
1505
+
1506
+ def create_sequence_and_trigger(table_name, options)
1507
+ seq_name = options[:sequence_name] || default_sequence_name(table_name)
1508
+ seq_start_value = options[:sequence_start_value] || default_sequence_start_value
1509
+ execute "CREATE SEQUENCE #{quote_table_name(seq_name)} START WITH #{seq_start_value}"
1510
+
1511
+ create_primary_key_trigger(table_name, options) if options[:primary_key_trigger]
1512
+ end
1513
+
1514
+ def create_primary_key_trigger(table_name, options)
1515
+ seq_name = options[:sequence_name] || default_sequence_name(table_name)
1516
+ trigger_name = options[:trigger_name] || default_trigger_name(table_name)
1517
+ primary_key = options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize)
1518
+ execute compress_lines(<<-SQL)
1519
+ CREATE OR REPLACE TRIGGER #{quote_table_name(trigger_name)}
1520
+ BEFORE INSERT ON #{quote_table_name(table_name)} FOR EACH ROW
1521
+ BEGIN
1522
+ IF inserting THEN
1523
+ IF :new.#{quote_column_name(primary_key)} IS NULL THEN
1524
+ SELECT #{quote_table_name(seq_name)}.NEXTVAL INTO :new.#{quote_column_name(primary_key)} FROM dual;
1525
+ END IF;
1526
+ END IF;
1527
+ END;
1528
+ SQL
1529
+ end
1530
+
1531
+ def default_trigger_name(table_name)
1532
+ # truncate table name if necessary to fit in max length of identifier
1533
+ "#{table_name.to_s[0,IDENTIFIER_MAX_LENGTH-4]}_pkt"
1534
+ end
1535
+
1536
+ def compress_lines(string, spaced = true)
1537
+ string.split($/).map { |line| line.strip }.join(spaced ? ' ' : '')
1538
+ end
1539
+
1540
+ # virtual columns are an 11g feature. This returns [] if feature is not
1541
+ # present or none are found.
1542
+ # return [{'column_name' => 'FOOS', 'data_default' => '...'}, ...]
1543
+ def virtual_columns_for(table)
1544
+ begin
1545
+ select_all <<-SQL
1546
+ select column_name, data_default
1547
+ from user_tab_cols
1548
+ where virtual_column='YES'
1549
+ and table_name='#{table.upcase}'
1550
+ SQL
1551
+ # feature not supported previous to 11g
1552
+ rescue ActiveRecord::StatementInvalid => e
1553
+ []
1554
+ end
1555
+ end
1556
+
1557
+ def drop_sql_for_feature(type)
1558
+ select_values("select 'DROP #{type.upcase} \"' || #{type}_name || '\";' from user_#{type.tableize}").join("\n\n")
1559
+ end
1560
+
1561
+ def drop_sql_for_object(type)
1562
+ select_values("select 'DROP #{type.upcase} ' || object_name || ';' from user_objects where object_type = '#{type.upcase}'").join("\n\n")
1563
+ end
1564
+
1565
+ public
1566
+ # DBMS_OUTPUT =============================================
1567
+ #
1568
+ # PL/SQL in Oracle uses dbms_output for logging print statements
1569
+ # These methods stick that output into the Rails log so Ruby and PL/SQL
1570
+ # code can can be debugged together in a single application
1571
+
1572
+ # Maximum DBMS_OUTPUT buffer size
1573
+ DBMS_OUTPUT_BUFFER_SIZE = 10000 # can be 1-1000000
1574
+
1575
+ # Turn DBMS_Output logging on
1576
+ def enable_dbms_output
1577
+ set_dbms_output_plsql_connection
1578
+ @enable_dbms_output = true
1579
+ plsql(:dbms_output).sys.dbms_output.enable(DBMS_OUTPUT_BUFFER_SIZE)
1580
+ end
1581
+ # Turn DBMS_Output logging off
1582
+ def disable_dbms_output
1583
+ set_dbms_output_plsql_connection
1584
+ @enable_dbms_output = false
1585
+ plsql(:dbms_output).sys.dbms_output.disable
1586
+ end
1587
+ # Is DBMS_Output logging enabled?
1588
+ def dbms_output_enabled?
1589
+ @enable_dbms_output
1590
+ end
1591
+
1592
+ protected
1593
+ def log(sql, name) #:nodoc:
1594
+ super sql, name
1595
+ ensure
1596
+ log_dbms_output if dbms_output_enabled?
1597
+ end
1598
+
1599
+ private
1600
+
1601
+ def set_dbms_output_plsql_connection
1602
+ raise OracleEnhancedConnectionException, "ruby-plsql gem is required for logging DBMS output" unless self.respond_to?(:plsql)
1603
+ # do not reset plsql connection if it is the same (as resetting will clear PL/SQL metadata cache)
1604
+ unless plsql(:dbms_output).connection && plsql(:dbms_output).connection.raw_connection == raw_connection
1605
+ plsql(:dbms_output).connection = raw_connection
1606
+ end
1607
+ end
1608
+
1609
+ def log_dbms_output
1610
+ while true do
1611
+ result = plsql(:dbms_output).sys.dbms_output.get_line(:line => '', :status => 0)
1612
+ break unless result[:status] == 0
1613
+ @logger.debug "DBMS_OUTPUT: #{result[:line]}"
1614
+ end
1615
+ end
1616
+
1617
+ end
1618
+ end
1619
+ end
1620
+
1621
+ # Added LOB writing callback for sessions stored in database
1622
+ # Otherwise it is not working as Session class is defined before OracleAdapter is loaded in Rails 2.0
1623
+ if defined?(CGI::Session::ActiveRecordStore::Session)
1624
+ if !CGI::Session::ActiveRecordStore::Session.respond_to?(:after_save_callback_chain) ||
1625
+ CGI::Session::ActiveRecordStore::Session.after_save_callback_chain.detect{|cb| cb.method == :enhanced_write_lobs}.nil?
1626
+ #:stopdoc:
1627
+ class CGI::Session::ActiveRecordStore::Session
1628
+ after_save :enhanced_write_lobs
1629
+ end
1630
+ #:startdoc:
1631
+ end
1632
+ end
1633
+
1634
+ # Load custom create, update, delete methods functionality
1635
+ require 'active_record/connection_adapters/oracle_enhanced_procedures'
1636
+
1637
+ # Load additional methods for composite_primary_keys support
1638
+ require 'active_record/connection_adapters/oracle_enhanced_cpk'
1639
+
1640
+ # Load patch for dirty tracking methods
1641
+ require 'active_record/connection_adapters/oracle_enhanced_dirty'
1642
+
1643
+ # Load rake tasks definitions
1644
+ begin
1645
+ require 'active_record/connection_adapters/oracle_enhanced_tasks'
1646
+ rescue LoadError
1647
+ end if defined?(RAILS_ROOT)
1648
+
1649
+ # Handles quoting of oracle reserved words
1650
+ require 'active_record/connection_adapters/oracle_enhanced_reserved_words'
1651
+
1652
+ # Patches and enhancements for schema dumper
1653
+ require 'active_record/connection_adapters/oracle_enhanced_schema_dumper'
1654
+
1655
+ # Extensions for schema definition statements
1656
+ require 'active_record/connection_adapters/oracle_enhanced_schema_statements_ext'
1657
+
1658
+ # Extensions for schema definition
1659
+ require 'active_record/connection_adapters/oracle_enhanced_schema_definitions'
1660
+
1661
+ # Add BigDecimal#to_d, Fixnum#to_d and Bignum#to_d methods if not already present
1662
+ require 'active_record/connection_adapters/oracle_enhanced_core_ext'
1663
+
1664
+ require 'active_record/connection_adapters/oracle_enhanced_version'