activerecord-oracle_enhanced-adapter 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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