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.
- data/.gitignore +3 -1
- data/Gemfile +37 -0
- data/History.txt +17 -0
- data/README.rdoc +9 -4
- data/RUNNING_TESTS.rdoc +28 -0
- data/VERSION +1 -1
- data/activerecord-oracle_enhanced-adapter.gemspec +13 -6
- data/lib/active_record/connection_adapters/oracle_enhanced.rake +32 -24
- data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +19 -793
- data/lib/active_record/connection_adapters/oracle_enhanced_base_ext.rb +79 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_column.rb +106 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_context_index.rb +32 -2
- data/lib/active_record/connection_adapters/oracle_enhanced_core_ext.rb +3 -44
- data/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb +2 -2
- data/lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb +4 -2
- data/lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb +19 -3
- data/lib/active_record/connection_adapters/oracle_enhanced_schema_dumper.rb +14 -6
- data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements.rb +331 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements_ext.rb +46 -14
- data/lib/active_record/connection_adapters/oracle_enhanced_structure_dump.rb +290 -0
- data/lib/active_record/connection_adapters/oracle_enhanced_tasks.rb +15 -10
- data/lib/activerecord-oracle_enhanced-adapter.rb +25 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +51 -19
- data/spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb +1 -1
- data/spec/active_record/connection_adapters/oracle_enhanced_context_index_spec.rb +31 -2
- data/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +152 -11
- data/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb +27 -0
- data/spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb +19 -1
- data/spec/active_record/connection_adapters/{oracle_enhanced_schema_spec.rb → oracle_enhanced_schema_statements_spec.rb} +44 -1
- data/spec/active_record/connection_adapters/{oracle_enhanced_adapter_structure_dumper_spec.rb → oracle_enhanced_structure_dump_spec.rb} +49 -1
- data/spec/spec_helper.rb +60 -53
- 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
|
-
|
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
|
-
|
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
|
-
|
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 ''
|
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
|
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]
|
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]
|
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
|
-
|
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[:
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|