activerecord-oracle_enhanced-adapter 1.1.9 → 1.2.0

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 (22) hide show
  1. data/History.txt +11 -0
  2. data/README.txt +6 -2
  3. data/lib/active_record/connection_adapters/emulation/oracle_adapter.rb +5 -0
  4. data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +771 -907
  5. data/lib/active_record/connection_adapters/oracle_enhanced_connection.rb +71 -0
  6. data/lib/active_record/connection_adapters/oracle_enhanced_core_ext.rb +64 -0
  7. data/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb +2 -2
  8. data/lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb +352 -0
  9. data/lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb +346 -0
  10. data/lib/active_record/connection_adapters/oracle_enhanced_procedures.rb +2 -1
  11. data/lib/active_record/connection_adapters/oracle_enhanced_reserved_words.rb +126 -0
  12. data/lib/active_record/connection_adapters/oracle_enhanced_version.rb +2 -2
  13. data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +200 -97
  14. data/spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb +170 -0
  15. data/spec/active_record/connection_adapters/oracle_enhanced_core_ext_spec.rb +40 -0
  16. data/spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb +11 -6
  17. data/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +148 -53
  18. data/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb +13 -5
  19. data/spec/active_record/connection_adapters/oracle_enhanced_emulate_oracle_adapter_spec.rb +27 -0
  20. data/spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb +10 -6
  21. data/spec/spec_helper.rb +51 -6
  22. metadata +11 -2
@@ -0,0 +1,71 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ # interface independent methods
4
+ class OracleEnhancedConnection
5
+
6
+ def self.create(config)
7
+ case ORACLE_ENHANCED_CONNECTION
8
+ when :oci
9
+ OracleEnhancedOCIConnection.new(config)
10
+ when :jdbc
11
+ OracleEnhancedJDBCConnection.new(config)
12
+ else
13
+ nil
14
+ end
15
+ end
16
+
17
+ attr_reader :raw_connection
18
+
19
+ # Oracle column names by default are case-insensitive, but treated as upcase;
20
+ # for neatness, we'll downcase within Rails. EXCEPT that folks CAN quote
21
+ # their column names when creating Oracle tables, which makes then case-sensitive.
22
+ # I don't know anybody who does this, but we'll handle the theoretical case of a
23
+ # camelCase column name. I imagine other dbs handle this different, since there's a
24
+ # unit test that's currently failing test_oci.
25
+ def oracle_downcase(column_name)
26
+ column_name =~ /[a-z]/ ? column_name : column_name.downcase
27
+ end
28
+
29
+ private
30
+
31
+ # Returns a record hash with the column names as keys and column values
32
+ # as values.
33
+ def select_one(sql)
34
+ result = select(sql)
35
+ result.first if result
36
+ end
37
+
38
+ # Returns a single value from a record
39
+ def select_value(sql)
40
+ if result = select_one(sql)
41
+ result.values.first
42
+ end
43
+ end
44
+
45
+ # Returns an array of the values of the first column in a select:
46
+ # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3]
47
+ def select_values(sql)
48
+ result = select(sql)
49
+ result.map { |r| r.values.first }
50
+ end
51
+
52
+
53
+ end
54
+
55
+ class OracleEnhancedConnectionException < StandardError
56
+ end
57
+
58
+ end
59
+ end
60
+
61
+ # if MRI or YARV
62
+ if !defined?(RUBY_ENGINE) || RUBY_ENGINE == 'ruby'
63
+ ORACLE_ENHANCED_CONNECTION = :oci
64
+ require 'active_record/connection_adapters/oracle_enhanced_oci_connection'
65
+ # if JRuby
66
+ elsif RUBY_ENGINE == 'jruby'
67
+ ORACLE_ENHANCED_CONNECTION = :jdbc
68
+ require 'active_record/connection_adapters/oracle_enhanced_jdbc_connection'
69
+ else
70
+ raise "Unsupported Ruby engine #{RUBY_ENGINE}"
71
+ end
@@ -0,0 +1,64 @@
1
+ require "bigdecimal"
2
+ unless BigDecimal.instance_methods.include?("to_d")
3
+ BigDecimal.class_eval do
4
+ def to_d
5
+ self
6
+ end
7
+ end
8
+ end
9
+
10
+ unless Bignum.instance_methods.include?("to_d")
11
+ Bignum.class_eval do
12
+ def to_d
13
+ BigDecimal.new(self.to_s)
14
+ end
15
+ end
16
+ end
17
+
18
+ unless Fixnum.instance_methods.include?("to_d")
19
+ Fixnum.class_eval do
20
+ def to_d
21
+ BigDecimal.new(self.to_s)
22
+ end
23
+ end
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
+ gem "unicode_utils", ">=1.0.0"
30
+ require "unicode_utils/upcase"
31
+ require "unicode_utils/downcase"
32
+
33
+ module ActiveRecord
34
+ module ConnectionAdapters
35
+ module OracleEnhancedUnicodeString
36
+ def upcase
37
+ UnicodeUtils.upcase(self)
38
+ end
39
+
40
+ def downcase
41
+ UnicodeUtils.downcase(self)
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ class String
48
+ def mb_chars
49
+ self.extend(ActiveRecord::ConnectionAdapters::OracleEnhancedUnicodeString)
50
+ self
51
+ end
52
+ end
53
+
54
+ rescue LoadError
55
+ warning_message = "WARNING: Please install unicode_utils gem to support Unicode aware upcase and downcase for String#mb_chars"
56
+ if defined?(RAILS_DEFAULT_LOGGER)
57
+ RAILS_DEFAULT_LOGGER.warn warning_message
58
+ else
59
+ STDERR.puts warning_message
60
+ end
61
+ end
62
+
63
+
64
+ end
@@ -8,12 +8,12 @@ module ActiveRecord #:nodoc:
8
8
  def field_changed?(attr, old, value)
9
9
  if column = column_for_attribute(attr)
10
10
  # RSI: added also :decimal type
11
- if (column.type == :integer || column.type == :decimal) && column.null && (old.nil? || old == 0)
11
+ if (column.type == :integer || column.type == :decimal) && column.null && (old.nil? || old == 0) && value.blank?
12
12
  # For nullable integer columns, NULL gets stored in database for blank (i.e. '') values.
13
13
  # Hence we don't record it as a change if the value changes from nil to ''.
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
- value = nil if value.blank?
16
+ value = nil
17
17
  # RSI: Oracle stores empty string '' or empty text (CLOB) as NULL
18
18
  # therefore need to convert empty string value to nil if old value is nil
19
19
  elsif (column.type == :string || column.type == :text) && column.null && old.nil?
@@ -0,0 +1,352 @@
1
+ begin
2
+ require "java"
3
+ require "jruby"
4
+ # Adds JRuby classloader to current thread classloader - as a result ojdbc14.jar should not be in $JRUBY_HOME/lib
5
+ java.lang.Thread.currentThread.setContextClassLoader(JRuby.runtime.jruby_class_loader)
6
+
7
+ ojdbc_jar = "ojdbc14.jar"
8
+ if ojdbc_jar_path = ENV["PATH"].split(/[:;]/).find{|d| File.exists?(File.join(d,ojdbc_jar))}
9
+ require File.join(ojdbc_jar_path,ojdbc_jar)
10
+ end
11
+ # import java.sql.Statement
12
+ # import java.sql.Connection
13
+ # import java.sql.SQLException
14
+ # import java.sql.Types
15
+ # import java.sql.DriverManager
16
+ java.sql.DriverManager.registerDriver Java::oracle.jdbc.driver.OracleDriver.new
17
+
18
+ rescue LoadError, NameError
19
+ # JDBC driver is unavailable.
20
+ error_message = "ERROR: ActiveRecord oracle_enhanced adapter could not load Oracle JDBC driver. "+
21
+ "Please install ojdbc14.jar library."
22
+ if defined?(RAILS_DEFAULT_LOGGER)
23
+ RAILS_DEFAULT_LOGGER.error error_message
24
+ else
25
+ STDERR.puts error_message
26
+ end
27
+ raise LoadError
28
+ end
29
+
30
+
31
+ module ActiveRecord
32
+ module ConnectionAdapters
33
+
34
+ # JDBC database interface for JRuby
35
+ class OracleEnhancedJDBCConnection < OracleEnhancedConnection
36
+
37
+ attr_accessor :active
38
+ alias :active? :active
39
+
40
+ attr_accessor :auto_retry
41
+ alias :auto_retry? :auto_retry
42
+ @auto_retry = false
43
+
44
+ def initialize(config)
45
+ @active = true
46
+ @config = config
47
+ new_connection(@config)
48
+ end
49
+
50
+ def new_connection(config)
51
+ username, password, database = config[:username].to_s, config[:password].to_s, config[:database].to_s
52
+ privilege = config[:privilege] && config[:privilege].to_s
53
+ host, port = config[:host], config[:port]
54
+
55
+ url = config[:url] || "jdbc:oracle:thin:@#{host || 'localhost'}:#{port || 1521}:#{database || 'XE'}"
56
+
57
+ prefetch_rows = config[:prefetch_rows] || 100
58
+ cursor_sharing = config[:cursor_sharing] || 'similar'
59
+
60
+ properties = java.util.Properties.new
61
+ properties.put("user", username)
62
+ properties.put("password", password)
63
+ properties.put("defaultRowPrefetch", "#{prefetch_rows}") if prefetch_rows
64
+ properties.put("internal_logon", privilege) if privilege
65
+
66
+ @raw_connection = java.sql.DriverManager.getConnection(url, properties)
67
+ exec %q{alter session set nls_date_format = 'YYYY-MM-DD HH24:MI:SS'}
68
+ exec %q{alter session set nls_timestamp_format = 'YYYY-MM-DD HH24:MI:SS'} # rescue nil
69
+ exec "alter session set cursor_sharing = #{cursor_sharing}" # rescue nil
70
+ self.autocommit = true
71
+
72
+ # Set session time zone to current time zone
73
+ @raw_connection.setSessionTimeZone(java.util.TimeZone.default.getID)
74
+
75
+ # default schema owner
76
+ @owner = username.upcase
77
+
78
+ @raw_connection
79
+ end
80
+
81
+
82
+ def logoff
83
+ @active = false
84
+ @raw_connection.close
85
+ true
86
+ rescue
87
+ false
88
+ end
89
+
90
+ def commit
91
+ @raw_connection.commit
92
+ end
93
+
94
+ def rollback
95
+ @raw_connection.rollback
96
+ end
97
+
98
+ def autocommit?
99
+ @raw_connection.getAutoCommit
100
+ end
101
+
102
+ def autocommit=(value)
103
+ @raw_connection.setAutoCommit(value)
104
+ end
105
+
106
+ # Checks connection, returns true if active. Note that ping actively
107
+ # checks the connection, while #active? simply returns the last
108
+ # known state.
109
+ def ping
110
+ exec_no_retry("select 1 from dual")
111
+ @active = true
112
+ rescue NativeException => e
113
+ @active = false
114
+ if e.message =~ /^java\.sql\.SQLException/
115
+ raise OracleEnhancedConnectionException, e.message
116
+ else
117
+ raise
118
+ end
119
+ end
120
+
121
+ # Resets connection, by logging off and creating a new connection.
122
+ def reset!
123
+ logoff rescue nil
124
+ begin
125
+ new_connection(@config)
126
+ @active = true
127
+ rescue NativeException => e
128
+ @active = false
129
+ if e.message =~ /^java\.sql\.SQLException/
130
+ raise OracleEnhancedConnectionException, e.message
131
+ else
132
+ raise
133
+ end
134
+ end
135
+ end
136
+
137
+ # mark connection as dead if connection lost
138
+ def with_retry(&block)
139
+ should_retry = auto_retry? && autocommit?
140
+ begin
141
+ yield if block_given?
142
+ rescue NativeException => e
143
+ raise unless e.message =~ /^java\.sql\.SQLException: (Closed Connection|Io exception:|No more data to read from socket)/
144
+ @active = false
145
+ raise unless should_retry
146
+ should_retry = false
147
+ reset! rescue nil
148
+ retry
149
+ end
150
+ end
151
+
152
+ def exec(sql)
153
+ with_retry do
154
+ exec_no_retry(sql)
155
+ end
156
+ end
157
+
158
+ def exec_no_retry(sql)
159
+ cs = prepare_call(sql)
160
+ case sql
161
+ when /\A\s*UPDATE/i, /\A\s*INSERT/i, /\A\s*DELETE/i
162
+ cs.executeUpdate
163
+ else
164
+ cs.execute
165
+ true
166
+ end
167
+ ensure
168
+ cs.close rescue nil
169
+ end
170
+
171
+ def select(sql, name = nil, return_column_names = false)
172
+ with_retry do
173
+ select_no_retry(sql, name, return_column_names)
174
+ end
175
+ end
176
+
177
+ def select_no_retry(sql, name = nil, return_column_names = false)
178
+ stmt = prepare_statement(sql)
179
+ rset = stmt.executeQuery
180
+ metadata = rset.getMetaData
181
+ column_count = metadata.getColumnCount
182
+ cols = (1..column_count).map do |i|
183
+ oracle_downcase(metadata.getColumnName(i))
184
+ end
185
+ col_types = (1..column_count).map do |i|
186
+ metadata.getColumnTypeName(i)
187
+ end
188
+
189
+ rows = []
190
+
191
+ while rset.next
192
+ hash = Hash.new
193
+
194
+ cols.each_with_index do |col, i0|
195
+ i = i0 + 1
196
+ hash[col] =
197
+ case column_type = col_types[i0]
198
+ when /CLOB/
199
+ name == 'Writable Large Object' ? rset.getClob(i) : get_ruby_value_from_result_set(rset, i, column_type)
200
+ when /BLOB/
201
+ name == 'Writable Large Object' ? rset.getBlob(i) : get_ruby_value_from_result_set(rset, i, column_type)
202
+ when 'DATE'
203
+ t = get_ruby_value_from_result_set(rset, i, column_type)
204
+ # RSI: added emulate_dates_by_column_name functionality
205
+ # if emulate_dates_by_column_name && self.class.is_date_column?(col)
206
+ # d.to_date
207
+ # elsif
208
+ if t && OracleEnhancedAdapter.emulate_dates && (t.hour == 0 && t.min == 0 && t.sec == 0)
209
+ t.to_date
210
+ else
211
+ # JRuby Time supports time before year 1900 therefore now need to fall back to DateTime
212
+ t
213
+ end
214
+ # RSI: added emulate_integers_by_column_name functionality
215
+ when "NUMBER"
216
+ n = get_ruby_value_from_result_set(rset, i, column_type)
217
+ if n && n.is_a?(Float) && OracleEnhancedAdapter.emulate_integers_by_column_name && OracleEnhancedAdapter.is_integer_column?(col)
218
+ n.to_i
219
+ else
220
+ n
221
+ end
222
+ else
223
+ get_ruby_value_from_result_set(rset, i, column_type)
224
+ end unless col == 'raw_rnum_'
225
+ end
226
+
227
+ rows << hash
228
+ end
229
+
230
+ return_column_names ? [rows, cols] : rows
231
+ ensure
232
+ rset.close rescue nil
233
+ stmt.close rescue nil
234
+ end
235
+
236
+ def write_lob(lob, value, is_binary = false)
237
+ if is_binary
238
+ lob.setBytes(1, value.to_java_bytes)
239
+ else
240
+ lob.setString(1,value)
241
+ end
242
+ end
243
+
244
+ def describe(name)
245
+ real_name = OracleEnhancedAdapter.valid_table_name?(name) ? name.to_s.upcase : name.to_s
246
+ if real_name.include?('.')
247
+ table_owner, table_name = real_name.split('.')
248
+ else
249
+ table_owner, table_name = @owner, real_name
250
+ end
251
+ sql = <<-SQL
252
+ SELECT owner, table_name, 'TABLE' name_type
253
+ FROM all_tables
254
+ WHERE owner = '#{table_owner}'
255
+ AND table_name = '#{table_name}'
256
+ UNION ALL
257
+ SELECT owner, view_name table_name, 'VIEW' name_type
258
+ FROM all_views
259
+ WHERE owner = '#{table_owner}'
260
+ AND view_name = '#{table_name}'
261
+ UNION ALL
262
+ SELECT table_owner, table_name, 'SYNONYM' name_type
263
+ FROM all_synonyms
264
+ WHERE owner = '#{table_owner}'
265
+ AND synonym_name = '#{table_name}'
266
+ UNION ALL
267
+ SELECT table_owner, table_name, 'SYNONYM' name_type
268
+ FROM all_synonyms
269
+ WHERE owner = 'PUBLIC'
270
+ AND synonym_name = '#{real_name}'
271
+ SQL
272
+ if result = select_one(sql)
273
+ case result['name_type']
274
+ when 'SYNONYM'
275
+ describe("#{result['owner']}.#{result['table_name']}")
276
+ else
277
+ [result['owner'], result['table_name']]
278
+ end
279
+ else
280
+ raise OracleEnhancedConnectionException, %Q{"DESC #{name}" failed; does it exist?}
281
+ end
282
+ end
283
+
284
+
285
+ private
286
+
287
+ def prepare_statement(sql)
288
+ @raw_connection.prepareStatement(sql)
289
+ end
290
+
291
+ def prepare_call(sql, *bindvars)
292
+ @raw_connection.prepareCall(sql)
293
+ end
294
+
295
+ def get_ruby_value_from_result_set(rset, i, type_name)
296
+ case type_name
297
+ when "CHAR", "VARCHAR2", "LONG"
298
+ rset.getString(i)
299
+ when "CLOB"
300
+ ora_value_to_ruby_value(rset.getClob(i))
301
+ when "BLOB"
302
+ ora_value_to_ruby_value(rset.getBlob(i))
303
+ when "NUMBER"
304
+ d = rset.getBigDecimal(i)
305
+ if d.nil?
306
+ nil
307
+ elsif d.scale == 0
308
+ d.toBigInteger+0
309
+ else
310
+ # Is there better way how to convert Java BigDecimal to Ruby BigDecimal?
311
+ d.toString.to_d
312
+ end
313
+ when "DATE"
314
+ if dt = rset.getDATE(i)
315
+ d = dt.dateValue
316
+ t = dt.timeValue
317
+ Time.send(Base.default_timezone, d.year + 1900, d.month + 1, d.date, t.hours, t.minutes, t.seconds)
318
+ else
319
+ nil
320
+ end
321
+ when /^TIMESTAMP/
322
+ ts = rset.getTimestamp(i)
323
+ ts && Time.send(Base.default_timezone, ts.year + 1900, ts.month + 1, ts.date, ts.hours, ts.minutes, ts.seconds,
324
+ ts.nanos / 1000)
325
+ else
326
+ nil
327
+ end
328
+ end
329
+
330
+ def ora_value_to_ruby_value(val)
331
+ case val
332
+ when ::Java::OracleSql::CLOB
333
+ if val.isEmptyLob
334
+ nil
335
+ else
336
+ val.getSubString(1, val.length)
337
+ end
338
+ when ::Java::OracleSql::BLOB
339
+ if val.isEmptyLob
340
+ nil
341
+ else
342
+ String.from_java_bytes(val.getBytes(1, val.length))
343
+ end
344
+ else
345
+ val
346
+ end
347
+ end
348
+
349
+ end
350
+
351
+ end
352
+ end