activerecord-oracle_enhanced-adapter 1.3.1 → 1.3.2

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 (35) hide show
  1. data/.rspec +2 -0
  2. data/Gemfile +35 -30
  3. data/History.txt +14 -0
  4. data/License.txt +1 -1
  5. data/README.rdoc +4 -1
  6. data/RUNNING_TESTS.rdoc +1 -1
  7. data/Rakefile +24 -26
  8. data/VERSION +1 -1
  9. data/activerecord-oracle_enhanced-adapter.gemspec +95 -68
  10. data/lib/active_record/connection_adapters/oracle_enhanced_activerecord_patches.rb +7 -3
  11. data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +147 -18
  12. data/lib/active_record/connection_adapters/oracle_enhanced_column.rb +31 -15
  13. data/lib/active_record/connection_adapters/oracle_enhanced_connection.rb +0 -2
  14. data/lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb +146 -51
  15. data/lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb +57 -18
  16. data/lib/active_record/connection_adapters/oracle_enhanced_procedures.rb +7 -1
  17. data/lib/active_record/connection_adapters/oracle_enhanced_schema_dumper.rb +6 -1
  18. data/lib/active_record/connection_adapters/oracle_enhanced_schema_statements.rb +6 -9
  19. data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +63 -3
  20. data/spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb +232 -151
  21. data/spec/active_record/connection_adapters/oracle_enhanced_context_index_spec.rb +5 -3
  22. data/spec/active_record/connection_adapters/oracle_enhanced_core_ext_spec.rb +1 -2
  23. data/spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb +1 -1
  24. data/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +61 -1
  25. data/spec/active_record/connection_adapters/oracle_enhanced_dbms_output_spec.rb +1 -1
  26. data/spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb +1 -1
  27. data/spec/active_record/connection_adapters/oracle_enhanced_emulate_oracle_adapter_spec.rb +1 -1
  28. data/spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb +1 -1
  29. data/spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb +10 -1
  30. data/spec/active_record/connection_adapters/oracle_enhanced_schema_statements_spec.rb +49 -4
  31. data/spec/active_record/connection_adapters/oracle_enhanced_structure_dump_spec.rb +1 -1
  32. data/spec/spec_helper.rb +7 -17
  33. metadata +160 -16
  34. data/.gitignore +0 -11
  35. data/spec/spec.opts +0 -6
@@ -2,12 +2,15 @@ module ActiveRecord
2
2
  module ConnectionAdapters #:nodoc:
3
3
  class OracleEnhancedColumn < Column
4
4
 
5
- attr_reader :table_name, :forced_column_type #:nodoc:
5
+ attr_reader :table_name, :forced_column_type, :nchar #:nodoc:
6
6
 
7
7
  def initialize(name, default, sql_type = nil, null = true, table_name = nil, forced_column_type = nil) #:nodoc:
8
8
  @table_name = table_name
9
9
  @forced_column_type = forced_column_type
10
10
  super(name, default, sql_type, null)
11
+ # Is column NCHAR or NVARCHAR2 (will need to use N'...' value quoting for these data types)?
12
+ # Define only when needed as adapter "quote" method will check at first if instance variable is defined.
13
+ @nchar = true if @type == :string && sql_type[0,1] == 'N'
11
14
  end
12
15
 
13
16
  def type_cast(value) #:nodoc:
@@ -46,26 +49,39 @@ module ActiveRecord
46
49
  end
47
50
 
48
51
  private
52
+
49
53
  def simplified_type(field_type)
50
54
  forced_column_type ||
51
55
  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)
56
+ when /decimal|numeric|number/i
57
+ if OracleEnhancedAdapter.emulate_booleans && field_type == 'NUMBER(1)'
58
+ :boolean
59
+ elsif extract_scale(field_type) == 0 ||
60
+ # if column name is ID or ends with _ID
61
+ OracleEnhancedAdapter.emulate_integers_by_column_name && OracleEnhancedAdapter.is_integer_column?(name, table_name)
62
+ :integer
63
+ else
57
64
  :decimal
58
- when /char/i
59
- return :boolean if OracleEnhancedAdapter.emulate_booleans_from_strings &&
60
- OracleEnhancedAdapter.is_boolean_column?(name, field_type, table_name)
65
+ end
66
+ when /char/i
67
+ if OracleEnhancedAdapter.emulate_booleans_from_strings &&
68
+ OracleEnhancedAdapter.is_boolean_column?(name, field_type, table_name)
69
+ :boolean
70
+ else
61
71
  :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)) ||
72
+ end
73
+ when /date/i
74
+ if OracleEnhancedAdapter.emulate_dates_by_column_name && OracleEnhancedAdapter.is_date_column?(name, table_name)
75
+ :date
76
+ else
65
77
  :datetime
66
- when /timestamp/i then :timestamp
67
- when /time/i then :datetime
68
- else super
78
+ end
79
+ when /timestamp/i
80
+ :timestamp
81
+ when /time/i
82
+ :datetime
83
+ else
84
+ super
69
85
  end
70
86
  end
71
87
 
@@ -77,8 +77,6 @@ module ActiveRecord
77
77
  end
78
78
  end
79
79
 
80
- private
81
-
82
80
  # Returns a record hash with the column names as keys and column values
83
81
  # as values.
84
82
  def select_one(sql)
@@ -49,12 +49,12 @@ module ActiveRecord
49
49
  # modified method to support JNDI connections
50
50
  def new_connection(config)
51
51
  username = nil
52
-
52
+
53
53
  if config[:jndi]
54
54
  jndi = config[:jndi].to_s
55
55
  ctx = javax.naming.InitialContext.new
56
56
  ds = nil
57
-
57
+
58
58
  # tomcat needs first lookup method, oc4j (and maybe other application servers) need second method
59
59
  begin
60
60
  env = ctx.lookup('java:/comp/env')
@@ -62,73 +62,90 @@ module ActiveRecord
62
62
  rescue
63
63
  ds = ctx.lookup(jndi)
64
64
  end
65
-
65
+
66
66
  # check if datasource supports pooled connections, otherwise use default
67
67
  if ds.respond_to?(:pooled_connection)
68
68
  @raw_connection = ds.pooled_connection
69
69
  else
70
70
  @raw_connection = ds.connection
71
71
  end
72
-
72
+
73
+ # get Oracle JDBC connection when using DBCP in Tomcat or jBoss
74
+ if @raw_connection.respond_to?(:getInnermostDelegate)
75
+ @pooled_connection = @raw_connection
76
+ @raw_connection = @raw_connection.innermost_delegate
77
+ elsif @raw_connection.respond_to?(:getUnderlyingConnection)
78
+ @pooled_connection = @raw_connection
79
+ @raw_connection = @raw_connection.underlying_connection
80
+ end
81
+
73
82
  config[:driver] ||= @raw_connection.meta_data.connection.java_class.name
74
83
  username = @raw_connection.meta_data.user_name
75
84
  else
76
- username, password, database = config[:username], config[:password], config[:database]
77
- privilege = config[:privilege] && config[:privilege].to_s
85
+ # to_s needed if username, password or database is specified as number in database.yml file
86
+ username = config[:username] && config[:username].to_s
87
+ password = config[:password] && config[:password].to_s
88
+ database = config[:database] && config[:database].to_s
78
89
  host, port = config[:host], config[:port]
79
-
90
+ privilege = config[:privilege] && config[:privilege].to_s
91
+
80
92
  # connection using TNS alias
81
93
  if database && !host && !config[:url] && ENV['TNS_ADMIN']
82
94
  url = "jdbc:oracle:thin:@#{database || 'XE'}"
83
95
  else
84
96
  url = config[:url] || "jdbc:oracle:thin:@#{host || 'localhost'}:#{port || 1521}:#{database || 'XE'}"
85
97
  end
86
-
98
+
87
99
  prefetch_rows = config[:prefetch_rows] || 100
88
100
  # get session time_zone from configuration or from TZ environment variable
89
101
  time_zone = config[:time_zone] || ENV['TZ'] || java.util.TimeZone.default.getID
90
-
102
+
91
103
  properties = java.util.Properties.new
92
104
  properties.put("user", username)
93
105
  properties.put("password", password)
94
106
  properties.put("defaultRowPrefetch", "#{prefetch_rows}") if prefetch_rows
95
107
  properties.put("internal_logon", privilege) if privilege
96
-
108
+
97
109
  @raw_connection = java.sql.DriverManager.getConnection(url, properties)
98
-
110
+
99
111
  # Set session time zone to current time zone
100
112
  @raw_connection.setSessionTimeZone(time_zone)
101
-
113
+
102
114
  # Set default number of rows to prefetch
103
115
  # @raw_connection.setDefaultRowPrefetch(prefetch_rows) if prefetch_rows
104
116
  end
105
117
 
106
- # by default VARCHAR2 column size will be interpreted as max number of characters (and not bytes)
107
- nls_length_semantics = config[:nls_length_semantics] || 'CHAR'
108
118
  cursor_sharing = config[:cursor_sharing] || 'force'
109
-
110
- # from here it remaings common for both connections types
111
- exec %q{alter session set nls_date_format = 'YYYY-MM-DD HH24:MI:SS'}
112
- exec %q{alter session set nls_timestamp_format = 'YYYY-MM-DD HH24:MI:SS:FF6'}
113
119
  exec "alter session set cursor_sharing = #{cursor_sharing}"
114
- exec "alter session set nls_length_semantics = '#{nls_length_semantics}'"
120
+
121
+ # Initialize NLS parameters
122
+ OracleEnhancedAdapter::DEFAULT_NLS_PARAMETERS.each do |key, default_value|
123
+ value = config[key] || ENV[key.to_s.upcase] || default_value
124
+ if value
125
+ exec "alter session set #{key} = '#{value}'"
126
+ end
127
+ end
128
+
115
129
  self.autocommit = true
116
-
130
+
117
131
  # default schema owner
118
132
  @owner = username.upcase unless username.nil?
119
-
133
+
120
134
  @raw_connection
121
135
  end
122
136
 
123
-
124
137
  def logoff
125
138
  @active = false
126
- @raw_connection.close
139
+ if defined?(@pooled_connection)
140
+ @pooled_connection.close
141
+ else
142
+ @raw_connection.close
143
+ end
127
144
  true
128
145
  rescue
129
146
  false
130
147
  end
131
-
148
+
132
149
  def commit
133
150
  @raw_connection.commit
134
151
  end
@@ -159,7 +176,7 @@ module ActiveRecord
159
176
  raise
160
177
  end
161
178
  end
162
-
179
+
163
180
  # Resets connection, by logging off and creating a new connection.
164
181
  def reset!
165
182
  logoff rescue nil
@@ -174,7 +191,7 @@ module ActiveRecord
174
191
  raise
175
192
  end
176
193
  end
177
- end
194
+ end
178
195
 
179
196
  # mark connection as dead if connection lost
180
197
  def with_retry(&block)
@@ -261,10 +278,100 @@ module ActiveRecord
261
278
  end
262
279
  end
263
280
 
281
+ def prepare(sql)
282
+ Cursor.new(self, @raw_connection.prepareStatement(sql))
283
+ end
284
+
285
+ class Cursor
286
+ def initialize(connection, raw_statement)
287
+ @connection = connection
288
+ @raw_statement = raw_statement
289
+ end
290
+
291
+ def bind_param(position, value)
292
+ java_value = ruby_to_java_value(value)
293
+ case value
294
+ when Integer
295
+ @raw_statement.setInt(position, java_value)
296
+ when Float
297
+ @raw_statement.setFloat(position, java_value)
298
+ when BigDecimal
299
+ @raw_statement.setBigDecimal(position, java_value)
300
+ when String
301
+ @raw_statement.setString(position, java_value)
302
+ when Date, DateTime
303
+ @raw_statement.setDATE(position, java_value)
304
+ when Time
305
+ @raw_statement.setTimestamp(position, java_value)
306
+ when NilClass
307
+ # TODO: currently nil is always bound as NULL with VARCHAR type.
308
+ # When nils will actually be used by ActiveRecord as bound parameters
309
+ # then need to pass actual column type.
310
+ @raw_statement.setNull(position, java.sql.Types::VARCHAR)
311
+ else
312
+ raise ArgumentError, "Don't know how to bind variable with type #{value.class}"
313
+ end
314
+ end
315
+
316
+ def exec
317
+ @raw_result_set = @raw_statement.executeQuery
318
+ get_metadata
319
+ true
320
+ end
321
+
322
+ def get_metadata
323
+ metadata = @raw_result_set.getMetaData
324
+ column_count = metadata.getColumnCount
325
+ @column_names = (1..column_count).map{|i| metadata.getColumnName(i)}
326
+ @column_types = (1..column_count).map{|i| metadata.getColumnTypeName(i).to_sym}
327
+ end
328
+
329
+ def get_col_names
330
+ @column_names
331
+ end
332
+
333
+ def fetch(options={})
334
+ if @raw_result_set.next
335
+ get_lob_value = options[:get_lob_value]
336
+ row_values = []
337
+ @column_types.each_with_index do |column_type, i|
338
+ row_values <<
339
+ @connection.get_ruby_value_from_result_set(@raw_result_set, i+1, column_type, get_lob_value)
340
+ end
341
+ row_values
342
+ else
343
+ @raw_result_set.close
344
+ nil
345
+ end
346
+ end
347
+
348
+ def close
349
+ @raw_statement.close
350
+ end
351
+
352
+ private
353
+
354
+ def ruby_to_java_value(value)
355
+ case value
356
+ when Fixnum, String, Float
357
+ value
358
+ when BigDecimal
359
+ java.math.BigDecimal.new(value.to_s)
360
+ when Date, DateTime
361
+ Java::oracle.sql.DATE.new(value.strftime("%Y-%m-%d %H:%M:%S"))
362
+ when Time
363
+ Java::java.sql.Timestamp.new(value.year-1900, value.month-1, value.day, value.hour, value.min, value.sec, value.usec * 1000)
364
+ else
365
+ value
366
+ end
367
+ end
368
+
369
+ end
370
+
264
371
  def select(sql, name = nil, return_column_names = false)
265
372
  with_retry do
266
373
  select_no_retry(sql, name, return_column_names)
267
- end
374
+ end
268
375
  end
269
376
 
270
377
  def select_no_retry(sql, name = nil, return_column_names = false)
@@ -276,7 +383,7 @@ module ActiveRecord
276
383
 
277
384
  metadata = rset.getMetaData
278
385
  column_count = metadata.getColumnCount
279
-
386
+
280
387
  cols_types_index = (1..column_count).map do |i|
281
388
  col_name = oracle_downcase(metadata.getColumnName(i))
282
389
  next if col_name == 'raw_rnum_'
@@ -287,7 +394,7 @@ module ActiveRecord
287
394
 
288
395
  rows = []
289
396
  get_lob_value = !(name == 'Writable Large Object')
290
-
397
+
291
398
  while rset.next
292
399
  hash = column_hash.dup
293
400
  cols_types_index.each do |col, column_type, i|
@@ -312,31 +419,17 @@ module ActiveRecord
312
419
 
313
420
  # Return NativeException / java.sql.SQLException error code
314
421
  def error_code(exception)
315
- exception.cause.getErrorCode
422
+ case exception
423
+ when NativeException
424
+ exception.cause.getErrorCode
425
+ else
426
+ nil
427
+ end
316
428
  end
317
429
 
318
- private
319
-
320
- # def prepare_statement(sql)
321
- # @raw_connection.prepareStatement(sql)
322
- # end
323
-
324
- # def prepare_call(sql, *bindvars)
325
- # @raw_connection.prepareCall(sql)
326
- # end
327
-
328
430
  def get_ruby_value_from_result_set(rset, i, type_name, get_lob_value = true)
329
431
  case type_name
330
432
  when :NUMBER
331
- # d = rset.getBigDecimal(i)
332
- # if d.nil?
333
- # nil
334
- # elsif d.scale == 0
335
- # d.toBigInteger+0
336
- # else
337
- # # Is there better way how to convert Java BigDecimal to Ruby BigDecimal?
338
- # d.toString.to_d
339
- # end
340
433
  d = rset.getNUMBER(i)
341
434
  if d.nil?
342
435
  nil
@@ -371,7 +464,9 @@ module ActiveRecord
371
464
  nil
372
465
  end
373
466
  end
374
-
467
+
468
+ private
469
+
375
470
  def lob_to_ruby_value(val)
376
471
  case val
377
472
  when ::Java::OracleSql::CLOB
@@ -390,6 +485,6 @@ module ActiveRecord
390
485
  end
391
486
 
392
487
  end
393
-
488
+
394
489
  end
395
490
  end
@@ -77,7 +77,7 @@ module ActiveRecord
77
77
  def exec(sql, *bindvars, &block)
78
78
  @raw_connection.exec(sql, *bindvars, &block)
79
79
  end
80
-
80
+
81
81
  def returning_clause(quoted_pk)
82
82
  " RETURNING #{quoted_pk} INTO :insert_id"
83
83
  end
@@ -93,6 +93,42 @@ module ActiveRecord
93
93
  cursor.close rescue nil
94
94
  end
95
95
 
96
+ def prepare(sql)
97
+ Cursor.new(self, @raw_connection.parse(sql))
98
+ end
99
+
100
+ class Cursor
101
+ def initialize(connection, raw_cursor)
102
+ @connection = connection
103
+ @raw_cursor = raw_cursor
104
+ end
105
+
106
+ def bind_param(position, value)
107
+ @raw_cursor.bind_param(position, value)
108
+ end
109
+
110
+ def exec
111
+ @raw_cursor.exec
112
+ end
113
+
114
+ def get_col_names
115
+ @raw_cursor.get_col_names
116
+ end
117
+
118
+ def fetch(options={})
119
+ if row = @raw_cursor.fetch
120
+ get_lob_value = options[:get_lob_value]
121
+ row.map do |col|
122
+ @connection.typecast_result_value(col, get_lob_value)
123
+ end
124
+ end
125
+ end
126
+
127
+ def close
128
+ @raw_cursor.close
129
+ end
130
+ end
131
+
96
132
  def select(sql, name = nil, return_column_names = false)
97
133
  cursor = @raw_connection.exec(sql)
98
134
  cols = []
@@ -146,8 +182,6 @@ module ActiveRecord
146
182
  end
147
183
  end
148
184
 
149
- private
150
-
151
185
  def typecast_result_value(value, get_lob_value)
152
186
  case value
153
187
  when Fixnum, Bignum
@@ -181,7 +215,9 @@ module ActiveRecord
181
215
  value
182
216
  end
183
217
  end
184
-
218
+
219
+ private
220
+
185
221
  def date_without_time?(value)
186
222
  case value
187
223
  when OraDate
@@ -215,41 +251,44 @@ module ActiveRecord
215
251
  # configure an Oracle/OCI connection.
216
252
  class OracleEnhancedOCIFactory #:nodoc:
217
253
  def self.new_connection(config)
218
- username, password, database = config[:username], config[:password], config[:database]
254
+ # to_s needed if username, password or database is specified as number in database.yml file
255
+ username = config[:username] && config[:username].to_s
256
+ password = config[:password] && config[:password].to_s
257
+ database = config[:database] && config[:database].to_s
219
258
  host, port = config[:host], config[:port]
220
259
  privilege = config[:privilege] && config[:privilege].to_sym
221
260
  async = config[:allow_concurrency]
222
261
  prefetch_rows = config[:prefetch_rows] || 100
223
262
  cursor_sharing = config[:cursor_sharing] || 'force'
224
- # by default VARCHAR2 column size will be interpreted as max number of characters (and not bytes)
225
- nls_length_semantics = config[:nls_length_semantics] || 'CHAR'
226
263
  # get session time_zone from configuration or from TZ environment variable
227
264
  time_zone = config[:time_zone] || ENV['TZ']
228
265
 
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
266
  # connection using host, port and database name
237
- else
267
+ connection_string = if host || port
238
268
  host ||= 'localhost'
239
269
  host = "[#{host}]" if host =~ /^[^\[].*:/ # IPv6
240
270
  port ||= 1521
241
271
  "//#{host}:#{port}/#{database}"
272
+ # if no host is specified then assume that
273
+ # database parameter is TNS alias or TNS connection string
274
+ else
275
+ database
242
276
  end
243
277
 
244
278
  conn = OCI8.new username, password, connection_string, privilege
245
- conn.exec %q{alter session set nls_date_format = 'YYYY-MM-DD HH24:MI:SS'}
246
- conn.exec %q{alter session set nls_timestamp_format = 'YYYY-MM-DD HH24:MI:SS:FF6'} rescue nil
247
279
  conn.autocommit = true
248
280
  conn.non_blocking = true if async
249
281
  conn.prefetch_rows = prefetch_rows
250
282
  conn.exec "alter session set cursor_sharing = #{cursor_sharing}" rescue nil
251
- conn.exec "alter session set nls_length_semantics = '#{nls_length_semantics}'"
252
283
  conn.exec "alter session set time_zone = '#{time_zone}'" unless time_zone.blank?
284
+
285
+ # Initialize NLS parameters
286
+ OracleEnhancedAdapter::DEFAULT_NLS_PARAMETERS.each do |key, default_value|
287
+ value = config[key] || ENV[key.to_s.upcase] || default_value
288
+ if value
289
+ conn.exec "alter session set #{key} = '#{value}'"
290
+ end
291
+ end
253
292
  conn
254
293
  end
255
294
  end