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.
- 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
|