plukevdh-activerecord-oracle_enhanced-adapter 1.2.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 (29) hide show
  1. data/History.txt +111 -0
  2. data/License.txt +20 -0
  3. data/Manifest.txt +26 -0
  4. data/README.rdoc +68 -0
  5. data/lib/active_record/connection_adapters/emulation/oracle_adapter.rb +5 -0
  6. data/lib/active_record/connection_adapters/oracle_enhanced.rake +48 -0
  7. data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +1200 -0
  8. data/lib/active_record/connection_adapters/oracle_enhanced_connection.rb +71 -0
  9. data/lib/active_record/connection_adapters/oracle_enhanced_core_ext.rb +64 -0
  10. data/lib/active_record/connection_adapters/oracle_enhanced_cpk.rb +21 -0
  11. data/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb +39 -0
  12. data/lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb +358 -0
  13. data/lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb +368 -0
  14. data/lib/active_record/connection_adapters/oracle_enhanced_procedures.rb +150 -0
  15. data/lib/active_record/connection_adapters/oracle_enhanced_reserved_words.rb +126 -0
  16. data/lib/active_record/connection_adapters/oracle_enhanced_tasks.rb +11 -0
  17. data/lib/active_record/connection_adapters/oracle_enhanced_version.rb +7 -0
  18. data/oracle-enhanced.gemspec +59 -0
  19. data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +659 -0
  20. data/spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb +170 -0
  21. data/spec/active_record/connection_adapters/oracle_enhanced_core_ext_spec.rb +40 -0
  22. data/spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb +103 -0
  23. data/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +951 -0
  24. data/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb +93 -0
  25. data/spec/active_record/connection_adapters/oracle_enhanced_emulate_oracle_adapter_spec.rb +27 -0
  26. data/spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb +340 -0
  27. data/spec/spec.opts +6 -0
  28. data/spec/spec_helper.rb +94 -0
  29. metadata +94 -0
@@ -0,0 +1,71 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ # interface independent methods
4
+ class OracleEnhancedConnection #:nodoc:
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 #:nodoc:
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 #:nodoc:
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 #:nodoc:
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 #:nodoc:
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 #:nodoc:
34
+ module ConnectionAdapters #:nodoc:
35
+ module OracleEnhancedUnicodeString #:nodoc:
36
+ def upcase #:nodoc:
37
+ UnicodeUtils.upcase(self)
38
+ end
39
+
40
+ def downcase #:nodoc:
41
+ UnicodeUtils.downcase(self)
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ class String #:nodoc:
48
+ def mb_chars #:nodoc:
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
@@ -0,0 +1,21 @@
1
+ module ActiveRecord #:nodoc:
2
+ module ConnectionAdapters #:nodoc:
3
+ module OracleEnhancedCpk #:nodoc:
4
+
5
+ # This mightn't be in Core, but count(distinct x,y) doesn't work for me.
6
+ # Return that not supported if composite_primary_keys gem is required.
7
+ def supports_count_distinct? #:nodoc:
8
+ @supports_count_distinct ||= ! defined?(CompositePrimaryKeys)
9
+ end
10
+
11
+ def concat(*columns) #:nodoc:
12
+ "(#{columns.join('||')})"
13
+ end
14
+
15
+ end
16
+ end
17
+ end
18
+
19
+ ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do
20
+ include ActiveRecord::ConnectionAdapters::OracleEnhancedCpk
21
+ end
@@ -0,0 +1,39 @@
1
+ module ActiveRecord #:nodoc:
2
+ module ConnectionAdapters #:nodoc:
3
+ module OracleEnhancedDirty #:nodoc:
4
+
5
+ module InstanceMethods #:nodoc:
6
+ private
7
+
8
+ def field_changed?(attr, old, value)
9
+ if column = column_for_attribute(attr)
10
+ # Added also :decimal type
11
+ if (column.type == :integer || column.type == :decimal) && column.null && (old.nil? || old == 0) && value.blank?
12
+ # For nullable integer columns, NULL gets stored in database for blank (i.e. '') values.
13
+ # Hence we don't record it as a change if the value changes from nil to ''.
14
+ # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll
15
+ # be typecast back to 0 (''.to_i => 0)
16
+ value = nil
17
+ # Oracle stores empty string '' or empty text (CLOB) as NULL
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?
20
+ value = nil if value == ''
21
+ else
22
+ value = column.type_cast(value)
23
+ end
24
+ end
25
+
26
+ old != value
27
+ end
28
+
29
+ end
30
+
31
+ end
32
+ end
33
+ end
34
+
35
+ if ActiveRecord::Base.instance_methods.include?('changed?')
36
+ ActiveRecord::Base.class_eval do
37
+ include ActiveRecord::ConnectionAdapters::OracleEnhancedDirty::InstanceMethods
38
+ end
39
+ end
@@ -0,0 +1,358 @@
1
+ begin
2
+ require "java"
3
+ require "jruby"
4
+
5
+ # ojdbc14.jar file should be in JRUBY_HOME/lib or should be in ENV['PATH'] or load path
6
+
7
+ ojdbc_jar = "ojdbc14.jar"
8
+
9
+ unless ENV_JAVA['java.class.path'] =~ Regexp.new(ojdbc_jar)
10
+ # Adds JRuby classloader to current thread classloader - as a result ojdbc14.jar should not be in $JRUBY_HOME/lib
11
+ # not necessary anymore for JRuby 1.3
12
+ # java.lang.Thread.currentThread.setContextClassLoader(JRuby.runtime.jruby_class_loader)
13
+
14
+ if ojdbc_jar_path = ENV["PATH"].split(/[:;]/).concat($LOAD_PATH).find{|d| File.exists?(File.join(d,ojdbc_jar))}
15
+ require File.join(ojdbc_jar_path,ojdbc_jar)
16
+ end
17
+ end
18
+
19
+ java.sql.DriverManager.registerDriver Java::oracle.jdbc.driver.OracleDriver.new
20
+
21
+ rescue LoadError, NameError
22
+ # JDBC driver is unavailable.
23
+ error_message = "ERROR: ActiveRecord oracle_enhanced adapter could not load Oracle JDBC driver. "+
24
+ "Please install ojdbc14.jar library."
25
+ if defined?(RAILS_DEFAULT_LOGGER)
26
+ RAILS_DEFAULT_LOGGER.error error_message
27
+ else
28
+ STDERR.puts error_message
29
+ end
30
+ raise LoadError
31
+ end
32
+
33
+
34
+ module ActiveRecord
35
+ module ConnectionAdapters
36
+
37
+ # JDBC database interface for JRuby
38
+ class OracleEnhancedJDBCConnection < OracleEnhancedConnection #:nodoc:
39
+
40
+ attr_accessor :active
41
+ alias :active? :active
42
+
43
+ attr_accessor :auto_retry
44
+ alias :auto_retry? :auto_retry
45
+ @auto_retry = false
46
+
47
+ def initialize(config)
48
+ @active = true
49
+ @config = config
50
+ new_connection(@config)
51
+ end
52
+
53
+ def new_connection(config)
54
+ username, password, database = config[:username].to_s, config[:password].to_s, config[:database].to_s
55
+ privilege = config[:privilege] && config[:privilege].to_s
56
+ host, port = config[:host], config[:port]
57
+
58
+ url = config[:url] || "jdbc:oracle:thin:@#{host || 'localhost'}:#{port || 1521}:#{database || 'XE'}"
59
+
60
+ prefetch_rows = config[:prefetch_rows] || 100
61
+ cursor_sharing = config[:cursor_sharing] || 'similar'
62
+ # by default VARCHAR2 column size will be interpreted as max number of characters (and not bytes)
63
+ nls_length_semantics = config[:nls_length_semantics] || 'CHAR'
64
+
65
+ properties = java.util.Properties.new
66
+ properties.put("user", username)
67
+ properties.put("password", password)
68
+ properties.put("defaultRowPrefetch", "#{prefetch_rows}") if prefetch_rows
69
+ properties.put("internal_logon", privilege) if privilege
70
+
71
+ @raw_connection = java.sql.DriverManager.getConnection(url, properties)
72
+ exec %q{alter session set nls_date_format = 'YYYY-MM-DD HH24:MI:SS'}
73
+ exec %q{alter session set nls_timestamp_format = 'YYYY-MM-DD HH24:MI:SS'}
74
+ exec "alter session set cursor_sharing = #{cursor_sharing}"
75
+ exec "alter session set nls_length_semantics = '#{nls_length_semantics}'"
76
+ self.autocommit = true
77
+
78
+ # Set session time zone to current time zone
79
+ @raw_connection.setSessionTimeZone(java.util.TimeZone.default.getID)
80
+
81
+ # Set default number of rows to prefetch
82
+ # @raw_connection.setDefaultRowPrefetch(prefetch_rows) if prefetch_rows
83
+
84
+ # default schema owner
85
+ @owner = username.upcase
86
+
87
+ @raw_connection
88
+ end
89
+
90
+
91
+ def logoff
92
+ @active = false
93
+ @raw_connection.close
94
+ true
95
+ rescue
96
+ false
97
+ end
98
+
99
+ def commit
100
+ @raw_connection.commit
101
+ end
102
+
103
+ def rollback
104
+ @raw_connection.rollback
105
+ end
106
+
107
+ def autocommit?
108
+ @raw_connection.getAutoCommit
109
+ end
110
+
111
+ def autocommit=(value)
112
+ @raw_connection.setAutoCommit(value)
113
+ end
114
+
115
+ # Checks connection, returns true if active. Note that ping actively
116
+ # checks the connection, while #active? simply returns the last
117
+ # known state.
118
+ def ping
119
+ exec_no_retry("select 1 from dual")
120
+ @active = true
121
+ rescue NativeException => e
122
+ @active = false
123
+ if e.message =~ /^java\.sql\.SQLException/
124
+ raise OracleEnhancedConnectionException, e.message
125
+ else
126
+ raise
127
+ end
128
+ end
129
+
130
+ # Resets connection, by logging off and creating a new connection.
131
+ def reset!
132
+ logoff rescue nil
133
+ begin
134
+ new_connection(@config)
135
+ @active = true
136
+ rescue NativeException => e
137
+ @active = false
138
+ if e.message =~ /^java\.sql\.SQLException/
139
+ raise OracleEnhancedConnectionException, e.message
140
+ else
141
+ raise
142
+ end
143
+ end
144
+ end
145
+
146
+ # mark connection as dead if connection lost
147
+ def with_retry(&block)
148
+ should_retry = auto_retry? && autocommit?
149
+ begin
150
+ yield if block_given?
151
+ rescue NativeException => e
152
+ raise unless e.message =~ /^java\.sql\.SQLException: (Closed Connection|Io exception:|No more data to read from socket)/
153
+ @active = false
154
+ raise unless should_retry
155
+ should_retry = false
156
+ reset! rescue nil
157
+ retry
158
+ end
159
+ end
160
+
161
+ def exec(sql)
162
+ with_retry do
163
+ exec_no_retry(sql)
164
+ end
165
+ end
166
+
167
+ def exec_no_retry(sql)
168
+ case sql
169
+ when /\A\s*(UPDATE|INSERT|DELETE)/i
170
+ s = @raw_connection.prepareStatement(sql)
171
+ s.executeUpdate
172
+ # it is safer for CREATE and DROP statements not to use PreparedStatement
173
+ # as it does not allow creation of triggers with :NEW in their definition
174
+ when /\A\s*(CREATE|DROP)/i
175
+ s = @raw_connection.createStatement()
176
+ s.execute(sql)
177
+ true
178
+ else
179
+ s = @raw_connection.prepareStatement(sql)
180
+ s.execute
181
+ true
182
+ end
183
+ ensure
184
+ s.close rescue nil
185
+ end
186
+
187
+ def select(sql, name = nil, return_column_names = false)
188
+ with_retry do
189
+ select_no_retry(sql, name, return_column_names)
190
+ end
191
+ end
192
+
193
+ def select_no_retry(sql, name = nil, return_column_names = false)
194
+ stmt = @raw_connection.prepareStatement(sql)
195
+ rset = stmt.executeQuery
196
+
197
+ # Reuse the same hash for all rows
198
+ column_hash = {}
199
+
200
+ metadata = rset.getMetaData
201
+ column_count = metadata.getColumnCount
202
+
203
+ cols_types_index = (1..column_count).map do |i|
204
+ col_name = oracle_downcase(metadata.getColumnName(i))
205
+ next if col_name == 'raw_rnum_'
206
+ column_hash[col_name] = nil
207
+ [col_name, metadata.getColumnTypeName(i).to_sym, i]
208
+ end
209
+ cols_types_index.delete(nil)
210
+
211
+ rows = []
212
+ get_lob_value = !(name == 'Writable Large Object')
213
+
214
+ while rset.next
215
+ hash = column_hash.dup
216
+ cols_types_index.each do |col, column_type, i|
217
+ hash[col] = get_ruby_value_from_result_set(rset, i, column_type, get_lob_value)
218
+ end
219
+ rows << hash
220
+ end
221
+
222
+ return_column_names ? [rows, cols_types_index.map(&:first)] : rows
223
+ ensure
224
+ rset.close rescue nil
225
+ stmt.close rescue nil
226
+ end
227
+
228
+ def write_lob(lob, value, is_binary = false)
229
+ if is_binary
230
+ lob.setBytes(1, value.to_java_bytes)
231
+ else
232
+ lob.setString(1,value)
233
+ end
234
+ end
235
+
236
+ def describe(name)
237
+ real_name = OracleEnhancedAdapter.valid_table_name?(name) ? name.to_s.upcase : name.to_s
238
+ if real_name.include?('.')
239
+ table_owner, table_name = real_name.split('.')
240
+ else
241
+ table_owner, table_name = @owner, real_name
242
+ end
243
+ sql = <<-SQL
244
+ SELECT owner, table_name, 'TABLE' name_type
245
+ FROM all_tables
246
+ WHERE owner = '#{table_owner}'
247
+ AND table_name = '#{table_name}'
248
+ UNION ALL
249
+ SELECT owner, view_name table_name, 'VIEW' name_type
250
+ FROM all_views
251
+ WHERE owner = '#{table_owner}'
252
+ AND view_name = '#{table_name}'
253
+ UNION ALL
254
+ SELECT table_owner, table_name, 'SYNONYM' name_type
255
+ FROM all_synonyms
256
+ WHERE owner = '#{table_owner}'
257
+ AND synonym_name = '#{table_name}'
258
+ UNION ALL
259
+ SELECT table_owner, table_name, 'SYNONYM' name_type
260
+ FROM all_synonyms
261
+ WHERE owner = 'PUBLIC'
262
+ AND synonym_name = '#{real_name}'
263
+ SQL
264
+ if result = select_one(sql)
265
+ case result['name_type']
266
+ when 'SYNONYM'
267
+ describe("#{result['owner']}.#{result['table_name']}")
268
+ else
269
+ [result['owner'], result['table_name']]
270
+ end
271
+ else
272
+ raise OracleEnhancedConnectionException, %Q{"DESC #{name}" failed; does it exist?}
273
+ end
274
+ end
275
+
276
+ # Return NativeException / java.sql.SQLException error code
277
+ def error_code(exception)
278
+ exception.cause.getErrorCode
279
+ end
280
+
281
+ private
282
+
283
+ # def prepare_statement(sql)
284
+ # @raw_connection.prepareStatement(sql)
285
+ # end
286
+
287
+ # def prepare_call(sql, *bindvars)
288
+ # @raw_connection.prepareCall(sql)
289
+ # end
290
+
291
+ def get_ruby_value_from_result_set(rset, i, type_name, get_lob_value = true)
292
+ case type_name
293
+ when :NUMBER
294
+ # d = rset.getBigDecimal(i)
295
+ # if d.nil?
296
+ # nil
297
+ # elsif d.scale == 0
298
+ # d.toBigInteger+0
299
+ # else
300
+ # # Is there better way how to convert Java BigDecimal to Ruby BigDecimal?
301
+ # d.toString.to_d
302
+ # end
303
+ d = rset.getNUMBER(i)
304
+ if d.nil?
305
+ nil
306
+ elsif d.isInt
307
+ Integer(d.stringValue)
308
+ else
309
+ BigDecimal.new(d.stringValue)
310
+ end
311
+ when :VARCHAR2, :CHAR, :LONG
312
+ rset.getString(i)
313
+ when :DATE
314
+ if dt = rset.getDATE(i)
315
+ d = dt.dateValue
316
+ t = dt.timeValue
317
+ if OracleEnhancedAdapter.emulate_dates && t.hours == 0 && t.minutes == 0 && t.seconds == 0
318
+ Date.new(d.year + 1900, d.month + 1, d.date)
319
+ else
320
+ Time.send(Base.default_timezone, d.year + 1900, d.month + 1, d.date, t.hours, t.minutes, t.seconds)
321
+ end
322
+ else
323
+ nil
324
+ end
325
+ when :TIMESTAMP, :TIMESTAMPTZ, :TIMESTAMPLTZ
326
+ ts = rset.getTimestamp(i)
327
+ ts && Time.send(Base.default_timezone, ts.year + 1900, ts.month + 1, ts.date, ts.hours, ts.minutes, ts.seconds,
328
+ ts.nanos / 1000)
329
+ when :CLOB
330
+ get_lob_value ? lob_to_ruby_value(rset.getClob(i)) : rset.getClob(i)
331
+ when :BLOB
332
+ get_lob_value ? lob_to_ruby_value(rset.getBlob(i)) : rset.getBlob(i)
333
+ else
334
+ nil
335
+ end
336
+ end
337
+
338
+ def lob_to_ruby_value(val)
339
+ case val
340
+ when ::Java::OracleSql::CLOB
341
+ if val.isEmptyLob
342
+ nil
343
+ else
344
+ val.getSubString(1, val.length)
345
+ end
346
+ when ::Java::OracleSql::BLOB
347
+ if val.isEmptyLob
348
+ nil
349
+ else
350
+ String.from_java_bytes(val.getBytes(1, val.length))
351
+ end
352
+ end
353
+ end
354
+
355
+ end
356
+
357
+ end
358
+ end