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,368 @@
1
+ require 'delegate'
2
+
3
+ begin
4
+ require 'oci8' unless self.class.const_defined? :OCI8
5
+
6
+ # added mapping for TIMESTAMP / WITH TIME ZONE / LOCAL TIME ZONE types
7
+ # currently Ruby-OCI8 does not support fractional seconds for timestamps
8
+ OCI8::BindType::Mapping[OCI8::SQLT_TIMESTAMP] = OCI8::BindType::OraDate
9
+ OCI8::BindType::Mapping[OCI8::SQLT_TIMESTAMP_TZ] = OCI8::BindType::OraDate
10
+ OCI8::BindType::Mapping[OCI8::SQLT_TIMESTAMP_LTZ] = OCI8::BindType::OraDate
11
+ rescue LoadError
12
+ # OCI8 driver is unavailable.
13
+ error_message = "ERROR: ActiveRecord oracle_enhanced adapter could not load ruby-oci8 library. "+
14
+ "Please install ruby-oci8 library or gem."
15
+ if defined?(RAILS_DEFAULT_LOGGER)
16
+ RAILS_DEFAULT_LOGGER.error error_message
17
+ else
18
+ STDERR.puts error_message
19
+ end
20
+ raise LoadError
21
+ end
22
+
23
+ module ActiveRecord
24
+ module ConnectionAdapters
25
+
26
+ # OCI database interface for MRI
27
+ class OracleEnhancedOCIConnection < OracleEnhancedConnection #:nodoc:
28
+
29
+ def initialize(config)
30
+ @raw_connection = OCI8EnhancedAutoRecover.new(config, OracleEnhancedOCIFactory)
31
+ end
32
+
33
+ def auto_retry
34
+ @raw_connection.auto_retry if @raw_connection
35
+ end
36
+
37
+ def auto_retry=(value)
38
+ @raw_connection.auto_retry = value if @raw_connection
39
+ end
40
+
41
+ def logoff
42
+ @raw_connection.logoff
43
+ @raw_connection.active = false
44
+ end
45
+
46
+ def commit
47
+ @raw_connection.commit
48
+ end
49
+
50
+ def rollback
51
+ @raw_connection.rollback
52
+ end
53
+
54
+ def autocommit?
55
+ @raw_connection.autocommit?
56
+ end
57
+
58
+ def autocommit=(value)
59
+ @raw_connection.autocommit = value
60
+ end
61
+
62
+ # Checks connection, returns true if active. Note that ping actively
63
+ # checks the connection, while #active? simply returns the last
64
+ # known state.
65
+ def ping
66
+ @raw_connection.ping
67
+ rescue OCIException => e
68
+ raise OracleEnhancedConnectionException, e.message
69
+ end
70
+
71
+ def active?
72
+ @raw_connection.active?
73
+ end
74
+
75
+ def reset!
76
+ @raw_connection.reset!
77
+ rescue OCIException => e
78
+ raise OracleEnhancedConnectionException, e.message
79
+ end
80
+
81
+ def exec(sql, *bindvars, &block)
82
+ @raw_connection.exec(sql, *bindvars, &block)
83
+ end
84
+
85
+ def select(sql, name = nil, return_column_names = false)
86
+ cursor = @raw_connection.exec(sql)
87
+ cols = []
88
+ # Ignore raw_rnum_ which is used to simulate LIMIT and OFFSET
89
+ cursor.get_col_names.each do |col_name|
90
+ col_name = oracle_downcase(col_name)
91
+ cols << col_name unless col_name == 'raw_rnum_'
92
+ end
93
+ # Reuse the same hash for all rows
94
+ column_hash = {}
95
+ cols.each {|c| column_hash[c] = nil}
96
+ rows = []
97
+ get_lob_value = !(name == 'Writable Large Object')
98
+
99
+ while row = cursor.fetch
100
+ hash = column_hash.dup
101
+
102
+ cols.each_with_index do |col, i|
103
+ hash[col] = typecast_result_value(row[i], get_lob_value)
104
+ end
105
+
106
+ rows << hash
107
+ end
108
+
109
+ return_column_names ? [rows, cols] : rows
110
+ ensure
111
+ cursor.close if cursor
112
+ end
113
+
114
+ def write_lob(lob, value, is_binary = false)
115
+ lob.write value
116
+ end
117
+
118
+ def describe(name)
119
+ quoted_name = OracleEnhancedAdapter.valid_table_name?(name) ? name : "\"#{name}\""
120
+ @raw_connection.describe(quoted_name)
121
+ rescue OCIException => e
122
+ raise OracleEnhancedConnectionException, e.message
123
+ end
124
+
125
+ # Return OCIError error code
126
+ def error_code(exception)
127
+ exception.code
128
+ end
129
+
130
+ private
131
+
132
+ def typecast_result_value(value, get_lob_value)
133
+ case value
134
+ when Fixnum, Bignum
135
+ value
136
+ when String
137
+ value
138
+ when Float
139
+ value == (v_to_i = value.to_i) ? v_to_i : value
140
+ # ruby-oci8 2.0 returns OraNumber if Oracle type is NUMBER
141
+ when OraNumber
142
+ value == (v_to_i = value.to_i) ? v_to_i : BigDecimal.new(value.to_s)
143
+ when OCI8::LOB
144
+ if get_lob_value
145
+ data = value.read
146
+ # In Ruby 1.9.1 always change encoding to ASCII-8BIT for binaries
147
+ data.force_encoding('ASCII-8BIT') if data.respond_to?(:force_encoding) && value.is_a?(OCI8::BLOB)
148
+ data
149
+ else
150
+ value
151
+ end
152
+ # ruby-oci8 1.0 returns OraDate
153
+ # ruby-oci8 2.0 returns Time or DateTime
154
+ when OraDate, Time, DateTime
155
+ if OracleEnhancedAdapter.emulate_dates && date_without_time?(value)
156
+ value.to_date
157
+ else
158
+ create_time_with_default_timezone(value)
159
+ end
160
+ else
161
+ value
162
+ end
163
+ end
164
+
165
+ def date_without_time?(value)
166
+ case value
167
+ when OraDate
168
+ value.hour == 0 && value.minute == 0 && value.second == 0
169
+ else
170
+ value.hour == 0 && value.min == 0 && value.sec == 0
171
+ end
172
+ end
173
+
174
+ def create_time_with_default_timezone(value)
175
+ year, month, day, hour, min, sec = case value
176
+ when OraDate
177
+ [value.year, value.month, value.day, value.hour, value.minute, value.second]
178
+ else
179
+ [value.year, value.month, value.day, value.hour, value.min, value.sec]
180
+ end
181
+ # code from Time.time_with_datetime_fallback
182
+ begin
183
+ Time.send(Base.default_timezone, year, month, day, hour, min, sec)
184
+ rescue
185
+ offset = Base.default_timezone.to_sym == :local ? ::DateTime.local_offset : 0
186
+ ::DateTime.civil(year, month, day, hour, min, sec, offset)
187
+ end
188
+ end
189
+ end
190
+
191
+ # The OracleEnhancedOCIFactory factors out the code necessary to connect and
192
+ # configure an Oracle/OCI connection.
193
+ class OracleEnhancedOCIFactory #:nodoc:
194
+ def self.new_connection(config)
195
+ username, password, database = config[:username].to_s, config[:password].to_s, config[:database].to_s
196
+ privilege = config[:privilege] && config[:privilege].to_sym
197
+ async = config[:allow_concurrency]
198
+ prefetch_rows = config[:prefetch_rows] || 100
199
+ cursor_sharing = config[:cursor_sharing] || 'similar'
200
+ # by default VARCHAR2 column size will be interpreted as max number of characters (and not bytes)
201
+ nls_length_semantics = config[:nls_length_semantics] || 'CHAR'
202
+
203
+ conn = OCI8.new username, password, database, privilege
204
+ conn.exec %q{alter session set nls_date_format = 'YYYY-MM-DD HH24:MI:SS'}
205
+ conn.exec %q{alter session set nls_timestamp_format = 'YYYY-MM-DD HH24:MI:SS'} rescue nil
206
+ conn.autocommit = true
207
+ conn.non_blocking = true if async
208
+ conn.prefetch_rows = prefetch_rows
209
+ conn.exec "alter session set cursor_sharing = #{cursor_sharing}" rescue nil
210
+ conn.exec "alter session set nls_length_semantics = '#{nls_length_semantics}'"
211
+ conn
212
+ end
213
+ end
214
+
215
+
216
+ end
217
+ end
218
+
219
+
220
+
221
+ class OCI8 #:nodoc:
222
+
223
+ class Cursor #:nodoc:
224
+ if method_defined? :define_a_column
225
+ # This OCI8 patch is required with the ruby-oci8 1.0.x or lower.
226
+ # Set OCI8::BindType::Mapping[] to change the column type
227
+ # when using ruby-oci8 2.0.
228
+
229
+ alias :enhanced_define_a_column_pre_ar :define_a_column
230
+ def define_a_column(i)
231
+ case do_ocicall(@ctx) { @parms[i - 1].attrGet(OCI_ATTR_DATA_TYPE) }
232
+ when 8; @stmt.defineByPos(i, String, 65535) # Read LONG values
233
+ when 187; @stmt.defineByPos(i, OraDate) # Read TIMESTAMP values
234
+ when 108
235
+ if @parms[i - 1].attrGet(OCI_ATTR_TYPE_NAME) == 'XMLTYPE'
236
+ @stmt.defineByPos(i, String, 65535)
237
+ else
238
+ raise 'unsupported datatype'
239
+ end
240
+ else enhanced_define_a_column_pre_ar i
241
+ end
242
+ end
243
+ end
244
+ end
245
+
246
+ if OCI8.public_method_defined?(:describe_table)
247
+ # ruby-oci8 2.0 or upper
248
+
249
+ def describe(name)
250
+ info = describe_table(name.to_s)
251
+ raise %Q{"DESC #{name}" failed} if info.nil?
252
+ [info.obj_schema, info.obj_name]
253
+ end
254
+ else
255
+ # ruby-oci8 1.0.x or lower
256
+
257
+ # missing constant from oci8 < 0.1.14
258
+ OCI_PTYPE_UNK = 0 unless defined?(OCI_PTYPE_UNK)
259
+
260
+ # Uses the describeAny OCI call to find the target owner and table_name
261
+ # indicated by +name+, parsing through synonynms as necessary. Returns
262
+ # an array of [owner, table_name].
263
+ def describe(name)
264
+ @desc ||= @@env.alloc(OCIDescribe)
265
+ @desc.attrSet(OCI_ATTR_DESC_PUBLIC, -1) if VERSION >= '0.1.14'
266
+ do_ocicall(@ctx) { @desc.describeAny(@svc, name.to_s, OCI_PTYPE_UNK) } rescue raise %Q{"DESC #{name}" failed; does it exist?}
267
+ info = @desc.attrGet(OCI_ATTR_PARAM)
268
+
269
+ case info.attrGet(OCI_ATTR_PTYPE)
270
+ when OCI_PTYPE_TABLE, OCI_PTYPE_VIEW
271
+ owner = info.attrGet(OCI_ATTR_OBJ_SCHEMA)
272
+ table_name = info.attrGet(OCI_ATTR_OBJ_NAME)
273
+ [owner, table_name]
274
+ when OCI_PTYPE_SYN
275
+ schema = info.attrGet(OCI_ATTR_SCHEMA_NAME)
276
+ name = info.attrGet(OCI_ATTR_NAME)
277
+ describe(schema + '.' + name)
278
+ else raise %Q{"DESC #{name}" failed; not a table or view.}
279
+ end
280
+ end
281
+ end
282
+
283
+ end
284
+
285
+ # The OCI8AutoRecover class enhances the OCI8 driver with auto-recover and
286
+ # reset functionality. If a call to #exec fails, and autocommit is turned on
287
+ # (ie., we're not in the middle of a longer transaction), it will
288
+ # automatically reconnect and try again. If autocommit is turned off,
289
+ # this would be dangerous (as the earlier part of the implied transaction
290
+ # may have failed silently if the connection died) -- so instead the
291
+ # connection is marked as dead, to be reconnected on it's next use.
292
+ #:stopdoc:
293
+ class OCI8EnhancedAutoRecover < DelegateClass(OCI8) #:nodoc:
294
+ attr_accessor :active #:nodoc:
295
+ alias :active? :active #:nodoc:
296
+
297
+ cattr_accessor :auto_retry
298
+ class << self
299
+ alias :auto_retry? :auto_retry #:nodoc:
300
+ end
301
+ @@auto_retry = false
302
+
303
+ def initialize(config, factory) #:nodoc:
304
+ @active = true
305
+ @config = config
306
+ @factory = factory
307
+ @connection = @factory.new_connection @config
308
+ super @connection
309
+ end
310
+
311
+ # Checks connection, returns true if active. Note that ping actively
312
+ # checks the connection, while #active? simply returns the last
313
+ # known state.
314
+ def ping #:nodoc:
315
+ @connection.exec("select 1 from dual") { |r| nil }
316
+ @active = true
317
+ rescue
318
+ @active = false
319
+ raise
320
+ end
321
+
322
+ # Resets connection, by logging off and creating a new connection.
323
+ def reset! #:nodoc:
324
+ logoff rescue nil
325
+ begin
326
+ @connection = @factory.new_connection @config
327
+ __setobj__ @connection
328
+ @active = true
329
+ rescue
330
+ @active = false
331
+ raise
332
+ end
333
+ end
334
+
335
+ # ORA-00028: your session has been killed
336
+ # ORA-01012: not logged on
337
+ # ORA-03113: end-of-file on communication channel
338
+ # ORA-03114: not connected to ORACLE
339
+ # ORA-03135: connection lost contact
340
+ LOST_CONNECTION_ERROR_CODES = [ 28, 1012, 3113, 3114, 3135 ] #:nodoc:
341
+
342
+ # Adds auto-recovery functionality.
343
+ #
344
+ # See: http://www.jiubao.org/ruby-oci8/api.en.html#label-11
345
+ def exec(sql, *bindvars, &block) #:nodoc:
346
+ should_retry = self.class.auto_retry? && autocommit?
347
+
348
+ begin
349
+ @connection.exec(sql, *bindvars, &block)
350
+ rescue OCIException => e
351
+ raise unless LOST_CONNECTION_ERROR_CODES.include?(e.code)
352
+ @active = false
353
+ raise unless should_retry
354
+ should_retry = false
355
+ reset! rescue nil
356
+ retry
357
+ end
358
+ end
359
+
360
+ # otherwise not working in Ruby 1.9.1
361
+ if RUBY_VERSION =~ /^1\.9/
362
+ def describe(name) #:nodoc:
363
+ @connection.describe(name)
364
+ end
365
+ end
366
+
367
+ end
368
+ #:startdoc:
@@ -0,0 +1,150 @@
1
+ # define accessors before requiring ruby-plsql as these accessors are used in clob writing callback and should be
2
+ # available also if ruby-plsql could not be loaded
3
+ ActiveRecord::Base.class_eval do
4
+ class_inheritable_accessor :custom_create_method, :custom_update_method, :custom_delete_method
5
+ end
6
+
7
+ require 'ruby_plsql'
8
+ require 'activesupport'
9
+
10
+ module ActiveRecord #:nodoc:
11
+ module ConnectionAdapters #:nodoc:
12
+ module OracleEnhancedProcedures #:nodoc:
13
+
14
+ module ClassMethods
15
+ # Specify custom create method which should be used instead of Rails generated INSERT statement.
16
+ # Provided block should return ID of new record.
17
+ # Example:
18
+ # set_create_method do
19
+ # plsql.employees_pkg.create_employee(
20
+ # :p_first_name => first_name,
21
+ # :p_last_name => last_name,
22
+ # :p_employee_id => nil
23
+ # )[:p_employee_id]
24
+ # end
25
+ def set_create_method(&block)
26
+ include_with_custom_methods
27
+ self.custom_create_method = block
28
+ end
29
+
30
+ # Specify custom update method which should be used instead of Rails generated UPDATE statement.
31
+ # Example:
32
+ # set_update_method do
33
+ # plsql.employees_pkg.update_employee(
34
+ # :p_employee_id => id,
35
+ # :p_first_name => first_name,
36
+ # :p_last_name => last_name
37
+ # )
38
+ # end
39
+ def set_update_method(&block)
40
+ include_with_custom_methods
41
+ self.custom_update_method = block
42
+ end
43
+
44
+ # Specify custom delete method which should be used instead of Rails generated DELETE statement.
45
+ # Example:
46
+ # set_delete_method do
47
+ # plsql.employees_pkg.delete_employee(
48
+ # :p_employee_id => id
49
+ # )
50
+ # end
51
+ def set_delete_method(&block)
52
+ include_with_custom_methods
53
+ self.custom_delete_method = block
54
+ end
55
+
56
+ private
57
+ def include_with_custom_methods
58
+ unless included_modules.include? InstanceMethods
59
+ include InstanceMethods
60
+ end
61
+ end
62
+ end
63
+
64
+ module InstanceMethods #:nodoc:
65
+ def self.included(base)
66
+ base.instance_eval do
67
+ if private_instance_methods.include?('create_without_callbacks') || private_instance_methods.include?(:create_without_callbacks)
68
+ alias_method :create_without_custom_method, :create_without_callbacks
69
+ alias_method :create_without_callbacks, :create_with_custom_method
70
+ else
71
+ alias_method_chain :create, :custom_method
72
+ end
73
+ # insert after dirty checking in Rails 2.1
74
+ # in Ruby 1.9 methods names are returned as symbols
75
+ if private_instance_methods.include?('update_without_dirty') || private_instance_methods.include?(:update_without_dirty)
76
+ alias_method :update_without_custom_method, :update_without_dirty
77
+ alias_method :update_without_dirty, :update_with_custom_method
78
+ elsif private_instance_methods.include?('update_without_callbacks') || private_instance_methods.include?(:update_without_callbacks)
79
+ alias_method :update_without_custom_method, :update_without_callbacks
80
+ alias_method :update_without_callbacks, :update_with_custom_method
81
+ else
82
+ alias_method_chain :update, :custom_method
83
+ end
84
+ private :create, :update
85
+ if public_instance_methods.include?('destroy_without_callbacks') || public_instance_methods.include?(:destroy_without_callbacks)
86
+ alias_method :destroy_without_custom_method, :destroy_without_callbacks
87
+ alias_method :destroy_without_callbacks, :destroy_with_custom_method
88
+ else
89
+ alias_method_chain :destroy, :custom_method
90
+ end
91
+ public :destroy
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ # Creates a record with custom create method
98
+ # and returns its id.
99
+ def create_with_custom_method
100
+ # check if class has custom create method
101
+ return create_without_custom_method unless self.class.custom_create_method
102
+ self.class.connection.log_custom_method("custom create method", "#{self.class.name} Create") do
103
+ self.id = self.class.custom_create_method.bind(self).call
104
+ end
105
+ @new_record = false
106
+ id
107
+ end
108
+
109
+ # Updates the associated record with custom update method
110
+ # Returns the number of affected rows.
111
+ def update_with_custom_method(attribute_names = @attributes.keys)
112
+ # check if class has custom create method
113
+ return update_without_custom_method unless self.class.custom_update_method
114
+ return 0 if attribute_names.empty?
115
+ self.class.connection.log_custom_method("custom update method with #{self.class.primary_key}=#{self.id}", "#{self.class.name} Update") do
116
+ self.class.custom_update_method.bind(self).call
117
+ end
118
+ 1
119
+ end
120
+
121
+ # Deletes the record in the database with custom delete method
122
+ # and freezes this instance to reflect that no changes should
123
+ # be made (since they can't be persisted).
124
+ def destroy_with_custom_method
125
+ # check if class has custom create method
126
+ return destroy_without_custom_method unless self.class.custom_delete_method
127
+ unless new_record?
128
+ self.class.connection.log_custom_method("custom delete method with #{self.class.primary_key}=#{self.id}", "#{self.class.name} Destroy") do
129
+ self.class.custom_delete_method.bind(self).call
130
+ end
131
+ end
132
+
133
+ freeze
134
+ end
135
+
136
+ end
137
+
138
+ end
139
+ end
140
+ end
141
+
142
+ ActiveRecord::Base.class_eval do
143
+ extend ActiveRecord::ConnectionAdapters::OracleEnhancedProcedures::ClassMethods
144
+ end
145
+
146
+ ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do
147
+ # public alias to log method which could be used from other objects
148
+ alias_method :log_custom_method, :log
149
+ public :log_custom_method
150
+ end