activerecord-oracle_enhanced-adapter 1.3.0 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. data/.gitignore +3 -1
  2. data/Gemfile +37 -0
  3. data/History.txt +17 -0
  4. data/README.rdoc +9 -4
  5. data/RUNNING_TESTS.rdoc +28 -0
  6. data/VERSION +1 -1
  7. data/activerecord-oracle_enhanced-adapter.gemspec +13 -6
  8. data/lib/active_record/connection_adapters/oracle_enhanced.rake +32 -24
  9. data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +19 -793
  10. data/lib/active_record/connection_adapters/oracle_enhanced_base_ext.rb +79 -0
  11. data/lib/active_record/connection_adapters/oracle_enhanced_column.rb +106 -0
  12. data/lib/active_record/connection_adapters/oracle_enhanced_context_index.rb +32 -2
  13. data/lib/active_record/connection_adapters/oracle_enhanced_core_ext.rb +3 -44
  14. data/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb +2 -2
  15. data/lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb +4 -2
  16. data/lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb +19 -3
  17. data/lib/active_record/connection_adapters/oracle_enhanced_schema_dumper.rb +14 -6
  18. data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements.rb +331 -0
  19. data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements_ext.rb +46 -14
  20. data/lib/active_record/connection_adapters/oracle_enhanced_structure_dump.rb +290 -0
  21. data/lib/active_record/connection_adapters/oracle_enhanced_tasks.rb +15 -10
  22. data/lib/activerecord-oracle_enhanced-adapter.rb +25 -0
  23. data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +51 -19
  24. data/spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb +1 -1
  25. data/spec/active_record/connection_adapters/oracle_enhanced_context_index_spec.rb +31 -2
  26. data/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +152 -11
  27. data/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb +27 -0
  28. data/spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb +19 -1
  29. data/spec/active_record/connection_adapters/{oracle_enhanced_schema_spec.rb → oracle_enhanced_schema_statements_spec.rb} +44 -1
  30. data/spec/active_record/connection_adapters/{oracle_enhanced_adapter_structure_dumper_spec.rb → oracle_enhanced_structure_dump_spec.rb} +49 -1
  31. data/spec/spec_helper.rb +60 -53
  32. metadata +15 -8
@@ -0,0 +1,79 @@
1
+ module ActiveRecord
2
+ class Base
3
+ # Establishes a connection to the database that's used by all Active Record objects.
4
+ def self.oracle_enhanced_connection(config) #:nodoc:
5
+ if config[:emulate_oracle_adapter] == true
6
+ # allows the enhanced adapter to look like the OracleAdapter. Useful to pick up
7
+ # conditionals in the rails activerecord test suite
8
+ require 'active_record/connection_adapters/emulation/oracle_adapter'
9
+ ConnectionAdapters::OracleAdapter.new(
10
+ ConnectionAdapters::OracleEnhancedConnection.create(config), logger)
11
+ else
12
+ ConnectionAdapters::OracleEnhancedAdapter.new(
13
+ ConnectionAdapters::OracleEnhancedConnection.create(config), logger)
14
+ end
15
+ end
16
+
17
+ # Specify table columns which should be ignored by ActiveRecord, e.g.:
18
+ #
19
+ # ignore_table_columns :attribute1, :attribute2
20
+ def self.ignore_table_columns(*args)
21
+ connection.ignore_table_columns(table_name,*args)
22
+ end
23
+
24
+ # Specify which table columns should be typecasted to Date (without time), e.g.:
25
+ #
26
+ # set_date_columns :created_on, :updated_on
27
+ def self.set_date_columns(*args)
28
+ connection.set_type_for_columns(table_name,:date,*args)
29
+ end
30
+
31
+ # Specify which table columns should be typecasted to Time (or DateTime), e.g.:
32
+ #
33
+ # set_datetime_columns :created_date, :updated_date
34
+ def self.set_datetime_columns(*args)
35
+ connection.set_type_for_columns(table_name,:datetime,*args)
36
+ end
37
+
38
+ # Specify which table columns should be typecasted to boolean values +true+ or +false+, e.g.:
39
+ #
40
+ # set_boolean_columns :is_valid, :is_completed
41
+ def self.set_boolean_columns(*args)
42
+ connection.set_type_for_columns(table_name,:boolean,*args)
43
+ end
44
+
45
+ # Specify which table columns should be typecasted to integer values.
46
+ # Might be useful to force NUMBER(1) column to be integer and not boolean, or force NUMBER column without
47
+ # scale to be retrieved as integer and not decimal. Example:
48
+ #
49
+ # set_integer_columns :version_number, :object_identifier
50
+ def self.set_integer_columns(*args)
51
+ connection.set_type_for_columns(table_name,:integer,*args)
52
+ end
53
+
54
+ # Specify which table columns should be typecasted to string values.
55
+ # Might be useful to specify that columns should be string even if its name matches boolean column criteria.
56
+ #
57
+ # set_string_columns :active_flag
58
+ def self.set_string_columns(*args)
59
+ connection.set_type_for_columns(table_name,:string,*args)
60
+ end
61
+
62
+ # After setting large objects to empty, select the OCI8::LOB
63
+ # and write back the data.
64
+ after_save :enhanced_write_lobs
65
+ def enhanced_write_lobs #:nodoc:
66
+ if connection.is_a?(ConnectionAdapters::OracleEnhancedAdapter) &&
67
+ !(self.class.custom_create_method || self.class.custom_update_method)
68
+ connection.write_lobs(self.class.table_name, self.class, attributes)
69
+ end
70
+ end
71
+ private :enhanced_write_lobs
72
+
73
+ # Get table comment from schema definition.
74
+ def self.table_comment
75
+ connection.table_comment(self.table_name)
76
+ end
77
+ end
78
+
79
+ end
@@ -0,0 +1,106 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters #:nodoc:
3
+ class OracleEnhancedColumn < Column
4
+
5
+ attr_reader :table_name, :forced_column_type #:nodoc:
6
+
7
+ def initialize(name, default, sql_type = nil, null = true, table_name = nil, forced_column_type = nil) #:nodoc:
8
+ @table_name = table_name
9
+ @forced_column_type = forced_column_type
10
+ super(name, default, sql_type, null)
11
+ end
12
+
13
+ def type_cast(value) #:nodoc:
14
+ return guess_date_or_time(value) if type == :datetime && OracleEnhancedAdapter.emulate_dates
15
+ super
16
+ end
17
+
18
+ # convert something to a boolean
19
+ # added y as boolean value
20
+ def self.value_to_boolean(value) #:nodoc:
21
+ if value == true || value == false
22
+ value
23
+ elsif value.is_a?(String) && value.blank?
24
+ nil
25
+ else
26
+ %w(true t 1 y +).include?(value.to_s.downcase)
27
+ end
28
+ end
29
+
30
+ # convert Time or DateTime value to Date for :date columns
31
+ def self.string_to_date(string) #:nodoc:
32
+ return string.to_date if string.is_a?(Time) || string.is_a?(DateTime)
33
+ super
34
+ end
35
+
36
+ # convert Date value to Time for :datetime columns
37
+ def self.string_to_time(string) #:nodoc:
38
+ return string.to_time if string.is_a?(Date) && !OracleEnhancedAdapter.emulate_dates
39
+ super
40
+ end
41
+
42
+ # Get column comment from schema definition.
43
+ # Will work only if using default ActiveRecord connection.
44
+ def comment
45
+ ActiveRecord::Base.connection.column_comment(@table_name, name)
46
+ end
47
+
48
+ private
49
+ def simplified_type(field_type)
50
+ forced_column_type ||
51
+ case field_type
52
+ when /decimal|numeric|number/i
53
+ return :boolean if OracleEnhancedAdapter.emulate_booleans && field_type == 'NUMBER(1)'
54
+ return :integer if extract_scale(field_type) == 0
55
+ # if column name is ID or ends with _ID
56
+ return :integer if OracleEnhancedAdapter.emulate_integers_by_column_name && OracleEnhancedAdapter.is_integer_column?(name, table_name)
57
+ :decimal
58
+ when /char/i
59
+ return :boolean if OracleEnhancedAdapter.emulate_booleans_from_strings &&
60
+ OracleEnhancedAdapter.is_boolean_column?(name, field_type, table_name)
61
+ :string
62
+ when /date/i
63
+ forced_column_type ||
64
+ (:date if OracleEnhancedAdapter.emulate_dates_by_column_name && OracleEnhancedAdapter.is_date_column?(name, table_name)) ||
65
+ :datetime
66
+ when /timestamp/i then :timestamp
67
+ when /time/i then :datetime
68
+ else super
69
+ end
70
+ end
71
+
72
+ def guess_date_or_time(value)
73
+ value.respond_to?(:hour) && (value.hour == 0 and value.min == 0 and value.sec == 0) ?
74
+ Date.new(value.year, value.month, value.day) : value
75
+ end
76
+
77
+ class << self
78
+ protected
79
+
80
+ def fallback_string_to_date(string) #:nodoc:
81
+ if OracleEnhancedAdapter.string_to_date_format || OracleEnhancedAdapter.string_to_time_format
82
+ return (string_to_date_or_time_using_format(string).to_date rescue super)
83
+ end
84
+ super
85
+ end
86
+
87
+ def fallback_string_to_time(string) #:nodoc:
88
+ if OracleEnhancedAdapter.string_to_time_format || OracleEnhancedAdapter.string_to_date_format
89
+ return (string_to_date_or_time_using_format(string).to_time rescue super)
90
+ end
91
+ super
92
+ end
93
+
94
+ def string_to_date_or_time_using_format(string) #:nodoc:
95
+ if OracleEnhancedAdapter.string_to_time_format && dt=Date._strptime(string, OracleEnhancedAdapter.string_to_time_format)
96
+ return Time.parse("#{dt[:year]}-#{dt[:mon]}-#{dt[:mday]} #{dt[:hour]}:#{dt[:min]}:#{dt[:sec]}#{dt[:zone]}")
97
+ end
98
+ DateTime.strptime(string, OracleEnhancedAdapter.string_to_date_format).to_date
99
+ end
100
+
101
+ end
102
+ end
103
+
104
+ end
105
+
106
+ end
@@ -37,8 +37,6 @@ module ActiveRecord
37
37
  # Post.contains(:all_text, 'word')
38
38
  #
39
39
  # ====== Creating index on multiple tables
40
- # add_context_index :posts, [:title, :body], :index_column => :all_text, :sync => 'ON COMMIT',
41
- # :index_column_trigger_on => [:created_at, :updated_at]
42
40
  # add_context_index :posts,
43
41
  # [:title, :body,
44
42
  # # specify aliases always with AS keyword
@@ -53,6 +51,9 @@ module ActiveRecord
53
51
  # Post.contains(:all_text, "aaa within title")
54
52
  # Post.contains(:all_text, "bbb within comment_author")
55
53
  #
54
+ # ====== Creating index using lexer
55
+ # add_context_index :posts, :title, :lexer => { :type => 'BASIC_LEXER', :base_letter => true, ... }
56
+ #
56
57
  def add_context_index(table_name, column_name, options = {})
57
58
  self.all_schema_indexes = nil
58
59
  column_names = Array(column_name)
@@ -86,6 +87,12 @@ module ActiveRecord
86
87
  if options[:sync]
87
88
  parameters << "SYNC(#{options[:sync]})"
88
89
  end
90
+ if options[:lexer] && (lexer_type = options[:lexer][:type])
91
+ lexer_name = default_lexer_name(index_name)
92
+ (lexer_options = options[:lexer].dup).delete(:type)
93
+ create_lexer_preference(lexer_name, lexer_type, lexer_options)
94
+ parameters << "LEXER #{lexer_name}"
95
+ end
89
96
  unless parameters.empty?
90
97
  sql << " PARAMETERS ('#{parameters.join(' ')}')"
91
98
  end
@@ -138,7 +145,9 @@ module ActiveRecord
138
145
  (column_names.map do |col|
139
146
  col = col.to_s
140
147
  "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+2}, '<#{col}>');\n" <<
148
+ "IF LENGTH(r1.#{col}) > 0 THEN\n" <<
141
149
  "DBMS_LOB.WRITEAPPEND(p_clob, LENGTH(r1.#{col}), r1.#{col});\n" <<
150
+ "END IF;\n" <<
142
151
  "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+3}, '</#{col}>');\n"
143
152
  end.join) <<
144
153
  (selected_columns.zip(select_queries).map do |cols, query|
@@ -204,6 +213,23 @@ module ActiveRecord
204
213
  execute sql
205
214
  end
206
215
 
216
+ def create_lexer_preference(lexer_name, lexer_type, options)
217
+ drop_ctx_preference(lexer_name)
218
+ sql = "BEGIN\nCTX_DDL.CREATE_PREFERENCE('#{lexer_name}', '#{lexer_type}');\n"
219
+ options.each do |key, value|
220
+ plsql_value = case value
221
+ when String; "'#{value}'"
222
+ when true; "'YES'"
223
+ when false; "'NO'"
224
+ when nil; 'NULL'
225
+ else value
226
+ end
227
+ sql << "CTX_DDL.SET_ATTRIBUTE('#{lexer_name}', '#{key}', #{plsql_value});\n"
228
+ end
229
+ sql << "END;\n"
230
+ execute sql
231
+ end
232
+
207
233
  def drop_ctx_preference(preference_name)
208
234
  execute "BEGIN CTX_DDL.DROP_PREFERENCE('#{preference_name}'); END;" rescue nil
209
235
  end
@@ -242,6 +268,10 @@ module ActiveRecord
242
268
  "#{index_name}_trg"
243
269
  end
244
270
 
271
+ def default_lexer_name(index_name)
272
+ "#{index_name}_lex"
273
+ end
274
+
245
275
  module BaseClassMethods
246
276
  # Declare that model table has context index defined.
247
277
  # As a result <tt>contains</tt> class scope method is defined.
@@ -1,5 +1,5 @@
1
1
  require "bigdecimal"
2
- unless BigDecimal.instance_methods.include?("to_d")
2
+ if (BigDecimal.instance_methods & ["to_d", :to_d]).empty?
3
3
  BigDecimal.class_eval do
4
4
  def to_d #:nodoc:
5
5
  self
@@ -7,7 +7,7 @@ unless BigDecimal.instance_methods.include?("to_d")
7
7
  end
8
8
  end
9
9
 
10
- unless Bignum.instance_methods.include?("to_d")
10
+ if (Bignum.instance_methods & ["to_d", :to_d]).empty?
11
11
  Bignum.class_eval do
12
12
  def to_d #:nodoc:
13
13
  BigDecimal.new(self.to_s)
@@ -15,51 +15,10 @@ unless Bignum.instance_methods.include?("to_d")
15
15
  end
16
16
  end
17
17
 
18
- unless Fixnum.instance_methods.include?("to_d")
18
+ if (Fixnum.instance_methods & ["to_d", :to_d]).empty?
19
19
  Fixnum.class_eval do
20
20
  def to_d #:nodoc:
21
21
  BigDecimal.new(self.to_s)
22
22
  end
23
23
  end
24
24
  end
25
-
26
- # Add Unicode aware String#upcase and String#downcase methods when mb_chars method is called
27
- if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'ruby' && RUBY_VERSION >= '1.9'
28
- begin
29
- require "unicode_utils/upcase"
30
- require "unicode_utils/downcase"
31
-
32
- module ActiveRecord #:nodoc:
33
- module ConnectionAdapters #:nodoc:
34
- module OracleEnhancedUnicodeString #:nodoc:
35
- def upcase #:nodoc:
36
- UnicodeUtils.upcase(self)
37
- end
38
-
39
- def downcase #:nodoc:
40
- UnicodeUtils.downcase(self)
41
- end
42
- end
43
- end
44
- end
45
-
46
- class String #:nodoc:
47
- def mb_chars #:nodoc:
48
- self.extend(ActiveRecord::ConnectionAdapters::OracleEnhancedUnicodeString)
49
- self
50
- end
51
- end
52
-
53
- rescue LoadError
54
- warning_message = "WARNING: Please install unicode_utils gem to support Unicode aware upcase and downcase for String#mb_chars"
55
- if defined?(Rails.logger) && Rails.logger
56
- Rails.logger.warn warning_message
57
- elsif defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER
58
- RAILS_DEFAULT_LOGGER.warn warning_message
59
- else
60
- STDERR.puts warning_message
61
- end
62
- end
63
-
64
-
65
- end
@@ -14,9 +14,9 @@ module ActiveRecord #:nodoc:
14
14
  # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll
15
15
  # be typecast back to 0 (''.to_i => 0)
16
16
  value = nil
17
- # Oracle stores empty string '' or empty text (CLOB) as NULL
17
+ # Oracle stores empty string '' as NULL
18
18
  # therefore need to convert empty string value to nil if old value is nil
19
- elsif (column.type == :string || column.type == :text) && column.null && old.nil?
19
+ elsif column.type == :string && column.null && old.nil?
20
20
  value = nil if value == ''
21
21
  else
22
22
  value = column.type_cast(value)
@@ -73,8 +73,7 @@ module ActiveRecord
73
73
  config[:driver] ||= @raw_connection.meta_data.connection.java_class.name
74
74
  username = @raw_connection.meta_data.user_name
75
75
  else
76
- username = config[:username].to_s
77
- password, database = config[:password].to_s, config[:database].to_s
76
+ username, password, database = config[:username], config[:password], config[:database]
78
77
  privilege = config[:privilege] && config[:privilege].to_s
79
78
  host, port = config[:host], config[:port]
80
79
 
@@ -207,6 +206,9 @@ module ActiveRecord
207
206
  # as it does not allow creation of triggers with :NEW in their definition
208
207
  when /\A\s*(CREATE|DROP)/i
209
208
  s = @raw_connection.createStatement()
209
+ # this disables SQL92 syntax processing of {...} which can result in statement execution errors
210
+ # if sql contains {...} in strings or comments
211
+ s.setEscapeProcessing(false)
210
212
  s.execute(sql)
211
213
  true
212
214
  else
@@ -162,7 +162,7 @@ module ActiveRecord
162
162
  value == (v_to_i = value.to_i) ? v_to_i : BigDecimal.new(value.to_s)
163
163
  when OCI8::LOB
164
164
  if get_lob_value
165
- data = value.read
165
+ data = value.read || "" # if value.read returns nil, then we have an empty_clob() i.e. an empty string
166
166
  # In Ruby 1.9.1 always change encoding to ASCII-8BIT for binaries
167
167
  data.force_encoding('ASCII-8BIT') if data.respond_to?(:force_encoding) && value.is_a?(OCI8::BLOB)
168
168
  data
@@ -215,7 +215,8 @@ module ActiveRecord
215
215
  # configure an Oracle/OCI connection.
216
216
  class OracleEnhancedOCIFactory #:nodoc:
217
217
  def self.new_connection(config)
218
- username, password, database = config[:username].to_s, config[:password].to_s, config[:database].to_s
218
+ username, password, database = config[:username], config[:password], config[:database]
219
+ host, port = config[:host], config[:port]
219
220
  privilege = config[:privilege] && config[:privilege].to_sym
220
221
  async = config[:allow_concurrency]
221
222
  prefetch_rows = config[:prefetch_rows] || 100
@@ -225,7 +226,22 @@ module ActiveRecord
225
226
  # get session time_zone from configuration or from TZ environment variable
226
227
  time_zone = config[:time_zone] || ENV['TZ']
227
228
 
228
- conn = OCI8.new username, password, database, privilege
229
+
230
+ # connection using TNS alias
231
+ connection_string = if database && !host && ENV['TNS_ADMIN']
232
+ database
233
+ # database parameter includes host or TNS connection string
234
+ elsif database =~ %r{[/(]}
235
+ database
236
+ # connection using host, port and database name
237
+ else
238
+ host ||= 'localhost'
239
+ host = "[#{host}]" if host =~ /^[^\[].*:/ # IPv6
240
+ port ||= 1521
241
+ "//#{host}:#{port}/#{database}"
242
+ end
243
+
244
+ conn = OCI8.new username, password, connection_string, privilege
229
245
  conn.exec %q{alter session set nls_date_format = 'YYYY-MM-DD HH24:MI:SS'}
230
246
  conn.exec %q{alter session set nls_timestamp_format = 'YYYY-MM-DD HH24:MI:SS:FF6'} rescue nil
231
247
  conn.autocommit = true
@@ -56,14 +56,22 @@ module ActiveRecord #:nodoc:
56
56
  add_foreign_key_statements = foreign_keys.map do |foreign_key|
57
57
  statement_parts = [ ('add_foreign_key ' + foreign_key.from_table.inspect) ]
58
58
  statement_parts << foreign_key.to_table.inspect
59
- statement_parts << (':name => ' + foreign_key.options[:name].inspect)
60
59
 
61
- if foreign_key.options[:column] != "#{foreign_key.to_table.singularize}_id"
62
- statement_parts << (':column => ' + foreign_key.options[:column].inspect)
63
- end
64
- if foreign_key.options[:primary_key] != 'id'
65
- statement_parts << (':primary_key => ' + foreign_key.options[:primary_key].inspect)
60
+ if foreign_key.options[:columns].size == 1
61
+ column = foreign_key.options[:columns].first
62
+ if column != "#{foreign_key.to_table.singularize}_id"
63
+ statement_parts << (':column => ' + column.inspect)
64
+ end
65
+
66
+ if foreign_key.options[:references].first != 'id'
67
+ statement_parts << (':primary_key => ' + foreign_key.options[:primary_key].inspect)
68
+ end
69
+ else
70
+ statement_parts << (':columns => ' + foreign_key.options[:columns].inspect)
66
71
  end
72
+
73
+ statement_parts << (':name => ' + foreign_key.options[:name].inspect)
74
+
67
75
  unless foreign_key.options[:dependent].blank?
68
76
  statement_parts << (':dependent => ' + foreign_key.options[:dependent].inspect)
69
77
  end
@@ -0,0 +1,331 @@
1
+ require 'digest/sha1'
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module OracleEnhancedSchemaStatements
6
+ # SCHEMA STATEMENTS ========================================
7
+ #
8
+ # see: abstract/schema_statements.rb
9
+
10
+ # Additional options for +create_table+ method in migration files.
11
+ #
12
+ # You can specify individual starting value in table creation migration file, e.g.:
13
+ #
14
+ # create_table :users, :sequence_start_value => 100 do |t|
15
+ # # ...
16
+ # end
17
+ #
18
+ # You can also specify other sequence definition additional parameters, e.g.:
19
+ #
20
+ # create_table :users, :sequence_start_value => “100 NOCACHE INCREMENT BY 10” do |t|
21
+ # # ...
22
+ # end
23
+ #
24
+ # Create primary key trigger (so that you can skip primary key value in INSERT statement).
25
+ # By default trigger name will be "table_name_pkt", you can override the name with
26
+ # :trigger_name option (but it is not recommended to override it as then this trigger will
27
+ # not be detected by ActiveRecord model and it will still do prefetching of sequence value).
28
+ # Example:
29
+ #
30
+ # create_table :users, :primary_key_trigger => true do |t|
31
+ # # ...
32
+ # end
33
+ #
34
+ # It is possible to add table and column comments in table creation migration files:
35
+ #
36
+ # create_table :employees, :comment => “Employees and contractors” do |t|
37
+ # t.string :first_name, :comment => “Given name”
38
+ # t.string :last_name, :comment => “Surname”
39
+ # end
40
+
41
+ def create_table(name, options = {}, &block)
42
+ create_sequence = options[:id] != false
43
+ column_comments = {}
44
+
45
+ table_definition = TableDefinition.new(self)
46
+ table_definition.primary_key(options[:primary_key] || Base.get_primary_key(name.to_s.singularize)) unless options[:id] == false
47
+
48
+ # store that primary key was defined in create_table block
49
+ unless create_sequence
50
+ class << table_definition
51
+ attr_accessor :create_sequence
52
+ def primary_key(*args)
53
+ self.create_sequence = true
54
+ super(*args)
55
+ end
56
+ end
57
+ end
58
+
59
+ # store column comments
60
+ class << table_definition
61
+ attr_accessor :column_comments
62
+ def column(name, type, options = {})
63
+ if options[:comment]
64
+ self.column_comments ||= {}
65
+ self.column_comments[name] = options[:comment]
66
+ end
67
+ super(name, type, options)
68
+ end
69
+ end
70
+
71
+ result = block.call(table_definition) if block
72
+ create_sequence = create_sequence || table_definition.create_sequence
73
+ column_comments = table_definition.column_comments if table_definition.column_comments
74
+ tablespace = options[:tablespace] ? " TABLESPACE #{options[:tablespace]}" : ""
75
+
76
+ if options[:force] && table_exists?(name)
77
+ drop_table(name, options)
78
+ end
79
+
80
+ create_sql = "CREATE#{' GLOBAL TEMPORARY' if options[:temporary]} TABLE "
81
+ create_sql << "#{quote_table_name(name)} ("
82
+ create_sql << table_definition.to_sql
83
+ create_sql << ")#{tablespace} #{options[:options]}"
84
+ execute create_sql
85
+
86
+ create_sequence_and_trigger(name, options) if create_sequence
87
+
88
+ add_table_comment name, options[:comment]
89
+ column_comments.each do |column_name, comment|
90
+ add_comment name, column_name, comment
91
+ end
92
+
93
+ end
94
+
95
+ def rename_table(name, new_name) #:nodoc:
96
+ execute "RENAME #{quote_table_name(name)} TO #{quote_table_name(new_name)}"
97
+ execute "RENAME #{quote_table_name("#{name}_seq")} TO #{quote_table_name("#{new_name}_seq")}" rescue nil
98
+ end
99
+
100
+ def drop_table(name, options = {}) #:nodoc:
101
+ super(name)
102
+ seq_name = options[:sequence_name] || default_sequence_name(name)
103
+ execute "DROP SEQUENCE #{quote_table_name(seq_name)}" rescue nil
104
+ ensure
105
+ clear_table_columns_cache(name)
106
+ end
107
+
108
+ # clear cached indexes when adding new index
109
+ def add_index(table_name, column_name, options = {}) #:nodoc:
110
+ column_names = Array(column_name)
111
+ index_name = index_name(table_name, :column => column_names)
112
+
113
+ if Hash === options # legacy support, since this param was a string
114
+ index_type = options[:unique] ? "UNIQUE" : ""
115
+ index_name = options[:name] || index_name
116
+ tablespace = options[:tablespace] ? " TABLESPACE #{options[:tablespace]}" : ""
117
+ else
118
+ index_type = options
119
+ end
120
+
121
+ if index_name.to_s.length > index_name_length
122
+ @logger.warn("Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters. Skipping.") if @logger
123
+ return
124
+ end
125
+ if index_name_exists?(table_name, index_name, false)
126
+ @logger.warn("Index name '#{index_name}' on table '#{table_name}' already exists. Skipping.") if @logger
127
+ return
128
+ end
129
+ quoted_column_names = column_names.map { |e| quote_column_name(e) }.join(", ")
130
+
131
+ execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{quoted_column_names})#{tablespace} #{options[:options]}"
132
+ ensure
133
+ self.all_schema_indexes = nil
134
+ end
135
+
136
+ # Remove the given index from the table.
137
+ # Gives warning if index does not exist
138
+ def remove_index(table_name, options = {}) #:nodoc:
139
+ index_name = index_name(table_name, options)
140
+ unless index_name_exists?(table_name, index_name, true)
141
+ @logger.warn("Index name '#{index_name}' on table '#{table_name}' does not exist. Skipping.") if @logger
142
+ return
143
+ end
144
+ remove_index!(table_name, index_name)
145
+ end
146
+
147
+ # clear cached indexes when removing index
148
+ def remove_index!(table_name, index_name) #:nodoc:
149
+ execute "DROP INDEX #{quote_column_name(index_name)}"
150
+ ensure
151
+ self.all_schema_indexes = nil
152
+ end
153
+
154
+ # returned shortened index name if default is too large
155
+ def index_name(table_name, options) #:nodoc:
156
+ default_name = super(table_name, options)
157
+ # sometimes options can be String or Array with column names
158
+ options = {} unless options.is_a?(Hash)
159
+ identifier_max_length = options[:identifier_max_length] || index_name_length
160
+ return default_name if default_name.length <= identifier_max_length
161
+
162
+ # remove 'index', 'on' and 'and' keywords
163
+ shortened_name = "i_#{table_name}_#{Array(options[:column]) * '_'}"
164
+
165
+ # leave just first three letters from each word
166
+ if shortened_name.length > identifier_max_length
167
+ shortened_name = shortened_name.split('_').map{|w| w[0,3]}.join('_')
168
+ end
169
+ # generate unique name using hash function
170
+ if shortened_name.length > identifier_max_length
171
+ shortened_name = 'i'+Digest::SHA1.hexdigest(default_name)[0,identifier_max_length-1]
172
+ end
173
+ @logger.warn "#{adapter_name} shortened default index name #{default_name} to #{shortened_name}" if @logger
174
+ shortened_name
175
+ end
176
+
177
+ # Verify the existence of an index with a given name.
178
+ #
179
+ # The default argument is returned if the underlying implementation does not define the indexes method,
180
+ # as there's no way to determine the correct answer in that case.
181
+ #
182
+ # Will always query database and not index cache.
183
+ def index_name_exists?(table_name, index_name, default)
184
+ (owner, table_name, db_link) = @connection.describe(table_name)
185
+ result = select_value(<<-SQL)
186
+ SELECT 1 FROM all_indexes#{db_link} i
187
+ WHERE i.owner = '#{owner}'
188
+ AND i.table_owner = '#{owner}'
189
+ AND i.table_name = '#{table_name}'
190
+ AND i.index_name = '#{index_name.to_s.upcase}'
191
+ SQL
192
+ result == 1 ? true : false
193
+ end
194
+
195
+ def add_column(table_name, column_name, type, options = {}) #:nodoc:
196
+ 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])}"
197
+ options[:type] = type
198
+ add_column_options!(add_column_sql, options)
199
+ execute(add_column_sql)
200
+ ensure
201
+ clear_table_columns_cache(table_name)
202
+ end
203
+
204
+ def change_column_default(table_name, column_name, default) #:nodoc:
205
+ execute "ALTER TABLE #{quote_table_name(table_name)} MODIFY #{quote_column_name(column_name)} DEFAULT #{quote(default)}"
206
+ ensure
207
+ clear_table_columns_cache(table_name)
208
+ end
209
+
210
+ def change_column_null(table_name, column_name, null, default = nil) #:nodoc:
211
+ column = column_for(table_name, column_name)
212
+
213
+ unless null || default.nil?
214
+ execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
215
+ end
216
+
217
+ change_column table_name, column_name, column.sql_type, :null => null
218
+ end
219
+
220
+ def change_column(table_name, column_name, type, options = {}) #:nodoc:
221
+ column = column_for(table_name, column_name)
222
+
223
+ # remove :null option if its value is the same as current column definition
224
+ # otherwise Oracle will raise error
225
+ if options.has_key?(:null) && options[:null] == column.null
226
+ options[:null] = nil
227
+ end
228
+
229
+ 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])}"
230
+ options[:type] = type
231
+ add_column_options!(change_column_sql, options)
232
+ execute(change_column_sql)
233
+ ensure
234
+ clear_table_columns_cache(table_name)
235
+ end
236
+
237
+ def rename_column(table_name, column_name, new_column_name) #:nodoc:
238
+ execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} to #{quote_column_name(new_column_name)}"
239
+ ensure
240
+ clear_table_columns_cache(table_name)
241
+ end
242
+
243
+ def remove_column(table_name, column_name) #:nodoc:
244
+ execute "ALTER TABLE #{quote_table_name(table_name)} DROP COLUMN #{quote_column_name(column_name)}"
245
+ ensure
246
+ clear_table_columns_cache(table_name)
247
+ end
248
+
249
+ def add_comment(table_name, column_name, comment) #:nodoc:
250
+ return if comment.blank?
251
+ execute "COMMENT ON COLUMN #{quote_table_name(table_name)}.#{column_name} IS '#{comment}'"
252
+ end
253
+
254
+ def add_table_comment(table_name, comment) #:nodoc:
255
+ return if comment.blank?
256
+ execute "COMMENT ON TABLE #{quote_table_name(table_name)} IS '#{comment}'"
257
+ end
258
+
259
+ def table_comment(table_name) #:nodoc:
260
+ (owner, table_name, db_link) = @connection.describe(table_name)
261
+ select_value <<-SQL
262
+ SELECT comments FROM all_tab_comments#{db_link}
263
+ WHERE owner = '#{owner}'
264
+ AND table_name = '#{table_name}'
265
+ SQL
266
+ end
267
+
268
+ def column_comment(table_name, column_name) #:nodoc:
269
+ (owner, table_name, db_link) = @connection.describe(table_name)
270
+ select_value <<-SQL
271
+ SELECT comments FROM all_col_comments#{db_link}
272
+ WHERE owner = '#{owner}'
273
+ AND table_name = '#{table_name}'
274
+ AND column_name = '#{column_name.upcase}'
275
+ SQL
276
+ end
277
+
278
+ # Maps logical Rails types to Oracle-specific data types.
279
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
280
+ # Ignore options for :text and :binary columns
281
+ return super(type, nil, nil, nil) if ['text', 'binary'].include?(type.to_s)
282
+
283
+ super
284
+ end
285
+
286
+ private
287
+
288
+ def column_for(table_name, column_name)
289
+ unless column = columns(table_name).find { |c| c.name == column_name.to_s }
290
+ raise "No such column: #{table_name}.#{column_name}"
291
+ end
292
+ column
293
+ end
294
+
295
+ def create_sequence_and_trigger(table_name, options)
296
+ seq_name = options[:sequence_name] || default_sequence_name(table_name)
297
+ seq_start_value = options[:sequence_start_value] || default_sequence_start_value
298
+ execute "CREATE SEQUENCE #{quote_table_name(seq_name)} START WITH #{seq_start_value}"
299
+
300
+ create_primary_key_trigger(table_name, options) if options[:primary_key_trigger]
301
+ end
302
+
303
+ def create_primary_key_trigger(table_name, options)
304
+ seq_name = options[:sequence_name] || default_sequence_name(table_name)
305
+ trigger_name = options[:trigger_name] || default_trigger_name(table_name)
306
+ primary_key = options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize)
307
+ execute compress_lines(<<-SQL)
308
+ CREATE OR REPLACE TRIGGER #{quote_table_name(trigger_name)}
309
+ BEFORE INSERT ON #{quote_table_name(table_name)} FOR EACH ROW
310
+ BEGIN
311
+ IF inserting THEN
312
+ IF :new.#{quote_column_name(primary_key)} IS NULL THEN
313
+ SELECT #{quote_table_name(seq_name)}.NEXTVAL INTO :new.#{quote_column_name(primary_key)} FROM dual;
314
+ END IF;
315
+ END IF;
316
+ END;
317
+ SQL
318
+ end
319
+
320
+ def default_trigger_name(table_name)
321
+ # truncate table name if necessary to fit in max length of identifier
322
+ "#{table_name.to_s[0,table_name_length-4]}_pkt"
323
+ end
324
+
325
+ end
326
+ end
327
+ end
328
+
329
+ ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do
330
+ include ActiveRecord::ConnectionAdapters::OracleEnhancedSchemaStatements
331
+ end